Part of Tuulbelt — A collection of zero-dependency tools.
Concurrent port allocation for any application — avoid port conflicts in tests, servers, microservices, and development environments.
Library Composition: This tool uses file-based-semaphore-ts for atomic registry access, following PRINCIPLES.md Exception 2. Since all Tuulbelt tools have zero external dependencies, composing them preserves the zero-dep guarantee.
Running concurrent processes that need ports often causes "port already in use" errors:
Error: listen EADDRINUSE: address already in use :::3000
This happens in many scenarios:
- Parallel tests: Multiple test files try to use the same hardcoded ports
- Development environments: Multiple services started simultaneously
- Microservices: Services don't know which ports other services are using
- CI/CD pipelines: Concurrent builds running on the same machine
- Docker/containers: Random port selection can still collide under high parallelism
portres solves this by providing a centralized port registry:
- Each process requests a port from the registry
- The registry ensures ports are unique across all processes
- Ports are automatically released when processes exit
- Works across languages, frameworks, and applications
- Zero external dependencies — Uses only Node.js standard library + Tuulbelt tools
- Tree-shakable & modular — Import only what you need with 8 entry points
- File-based registry — Survives process restarts
- Contiguous port ranges — Reserve adjacent ports for microservices clusters
- Bounded allocation — Get ports within specific ranges (firewall rules, compliance)
- Module-level convenience APIs — Simple
getPort()andgetPorts()functions - Port lifecycle management — Track allocations by tag with
PortManager - Batch allocation with rollback — All-or-nothing semantics for atomic operations
- Stale entry cleanup — Automatically removes dead process entries
- Semaphore-protected registry — Atomic access via semats
- Result pattern — Clear error handling without exceptions
- CLI and library API — Use from shell or TypeScript
Port Resolver v0.3.0 is fully modularized with 8 entry points for optimal tree-shaking:
// Main entry - everything (default)
import { PortResolver, getPort } from '@tuulbelt/port-resolver';
// Import only core classes (saves ~40% bundle size)
import { PortResolver, PortManager } from '@tuulbelt/port-resolver/core';
// Import only convenience APIs (saves ~65% bundle size)
import { getPort, getPorts, releasePort } from '@tuulbelt/port-resolver/api';
// Import only utilities (saves ~80% bundle size)
import { sanitizeTag, validatePath } from '@tuulbelt/port-resolver/utils';
// Import only types (zero runtime code)
import type { PortConfig, PortEntry } from '@tuulbelt/port-resolver/types';Available entry points:
.— Full API (default)/core—PortResolver,PortManagerclasses/api—getPort(),getPorts(),releasePort()functions/utils— Helper utilities (sanitizeTag,validatePath, etc.)/registry— Registry operations (readRegistry,writeRegistry, etc.)/types— TypeScript type definitions (no runtime code)/config— Configuration constants (DEFAULT_CONFIG, etc.)
Bundle size comparison:
| Import | Bundle Size (minified) | vs Full API |
|---|---|---|
| Full API | ~28 KB | baseline |
| Core only | ~17 KB | -40% |
| API only | ~10 KB | -65% |
| Utils only | ~5 KB | -80% |
How it works:
package.jsonexports field defines entry pointssideEffects: falseenables aggressive tree-shaking- Each module is independently importable
- No code duplication (shared utilities factored out)
Clone the repository:
git clone https://github.com/tuulbelt/port-resolver.git
cd port-resolver
npm install # Installs dev dependencies + auto-fetches file-based-semaphore-ts from GitHubZero external dependencies — uses only Node.js standard library and file-based-semaphore-ts (a Tuulbelt tool with zero external deps).
CLI names — both short and long forms work:
- Short (recommended):
portres - Long:
port-resolver
Recommended setup — install globally for easy access:
npm link # Enable the 'portres' command globally
portres --helpFor local development without global install:
npx tsx src/cli.ts --helpBasic Usage (Class API):
import { PortResolver } from './src/index.ts';
const resolver = new PortResolver();
// Get a single port
const result = await resolver.get({ tag: 'my-test-server' });
if (result.ok) {
console.log(`Using port: ${result.value.port}`);
// Start your server on result.value.port
// Release when done
await resolver.release(result.value.port);
}
// Get multiple ports at once
const ports = await resolver.getMultiple(3);
if (ports.ok) {
console.log(`Got ports: ${ports.value.map(p => p.port).join(', ')}`);
}
// Release all ports at end of test suite
await resolver.releaseAll();v0.2.0 New APIs:
import { getPort, getPorts, PortManager, PortResolver } from './src/index.ts';
// Module-level convenience API (no class instantiation needed)
const port = await getPort({ tag: 'api-server' });
if (port.ok) {
console.log(`API server port: ${port.value.port}`);
}
// Batch allocation with individual tags
const services = await getPorts(3, {
tags: ['http-server', 'grpc-server', 'metrics-server'],
});
if (services.ok) {
console.log('Service ports:', services.value.map(p => `${p.tag}: ${p.port}`));
}
// Reserve contiguous port range (for microservices cluster)
const resolver = new PortResolver();
const cluster = await resolver.reserveRange({
start: 50000,
count: 5,
tag: 'backend-cluster',
});
if (cluster.ok) {
console.log('Cluster ports:', cluster.value.map(p => p.port).join(', '));
// Allocated: 50000, 50001, 50002, 50003, 50004 (contiguous)
}
// Get port within specific range (for firewall/compliance requirements)
const apiPort = await resolver.getPortInRange({
min: 8000,
max: 9000,
tag: 'public-api',
});
if (apiPort.ok) {
console.log(`Public API port (8000-9000): ${apiPort.value.port}`);
}
// Port lifecycle management with PortManager
const manager = new PortManager();
await manager.allocate('frontend');
await manager.allocate('backend');
await manager.allocate('database');
// Access by tag
const frontend = manager.get('frontend');
console.log(`Frontend port: ${frontend?.port}`);
// Release by tag instead of port number
await manager.release('frontend');
// Release all managed ports
await manager.releaseAll();Using short name (recommended after npm link):
# Get one available port
portres get
# 51234
# Get 3 ports at once
portres get -n 3
# 51234
# 51235
# 51236
# Get port with tag (for identification)
portres get -t my-server --json
# {"port":51234,"tag":"my-server"}
# Release a port
portres release 51234
# Release all ports for current process
portres release-all
# List all allocations
portres list
# Port PID Tag Timestamp
# 51234 12345 my-server 2025-12-29T01:00:00.000Z
# Show registry status
portres status
# Registry Status:
# Total entries: 1
# Active entries: 1
# Stale entries: 0
# Owned by this process: 1
# Port range: 49152-65535
# Clean stale entries (dead processes)
portres clean
# Clear entire registry
portres clear
# v0.2.0: Reserve contiguous port range
portres reserve-range -p 50000 -n 5 -t cluster
# Allocated: 50000, 50001, 50002, 50003, 50004
# v0.2.0: Get port within specific range
portres get-in-range --min-port 8000 --max-port 9000 -t api
# 8042Main class for port allocation.
const resolver = new PortResolver({
minPort: 49152, // Minimum port (default: 49152)
maxPort: 65535, // Maximum port (default: 65535)
registryDir: '~/.portres', // Registry directory
allowPrivileged: false, // Allow ports < 1024
maxPortsPerRequest: 100, // Max ports per getMultiple()
maxRegistrySize: 1000, // Max total entries
staleTimeout: 3600000, // Stale entry timeout (1 hour)
});Core Methods:
| Method | Description |
|---|---|
get(options?) |
Get a single available port |
getMultiple(count, options?) |
Get multiple ports at once |
release(port) |
Release a specific port |
releaseAll() |
Release all ports owned by this process |
list() |
List all port allocations |
clean() |
Remove stale entries |
status() |
Get registry status |
clear() |
Clear entire registry |
v0.2.0 New Methods:
| Method | Description |
|---|---|
reserveRange(options) |
Reserve contiguous port range (e.g., 50000-50004) |
getPortInRange(options) |
Get any port within specific bounds (e.g., 8000-9000) |
| Function | Description |
|---|---|
getPort(options?) |
Convenience wrapper for single port allocation |
getPorts(count, options?) |
Batch allocation with individual tags or shared tag |
Track and manage port allocations by tag with lifecycle management:
| Method | Description |
|---|---|
allocate(tag) |
Allocate port and track by tag. Prevents duplicate tags within this instance. |
allocateMultiple(count, tag?) |
Allocate multiple ports atomically. All share same tag if provided. |
release(tagOrPort) |
Release by tag or port number. Idempotent - succeeds even if already released. |
releaseAll() |
Release all managed ports. Returns count of ports released. |
getAllocations() |
Get all tracked allocations as array of PortAllocation objects. |
get(tag) |
Get specific allocation by tag. Returns undefined if not found. |
Important Behavior Notes:
- Tags are per-instance: Each
PortManagerinstance has independent tag tracking. Two instances can use the same tag (they'll get different ports). - Duplicate tag prevention: Calling
allocate(tag)twice with the same tag will fail to prevent losing track of the first allocation. - Idempotent release:
release(tag)succeeds even if the tag was never allocated or already released. - Registry is shared: All
PortManagerinstances share the same registry file, so allocated ports are globally tracked even if tags are independent.
Check if a port is available by attempting to bind to it.
const available = await isPortAvailable(3000);
if (available) {
// Port 3000 is free to use
}See the examples/ directory for runnable examples:
# Fundamentals
npx tsx examples/basic.ts # Basic usage
npx tsx examples/advanced.ts # Advanced patterns
# v0.2.0 New APIs
npx tsx examples/parallel-tests.ts # Parallel test execution patterns
npx tsx examples/batch-allocation.ts # Module-level APIs (getPort, getPorts)
npx tsx examples/port-manager.ts # Lifecycle management with PortManager
npx tsx examples/ci-integration.ts # CI/CD integration patterns
# v0.3.0 Modularization
npx tsx examples/modular-imports.ts # Tree-shaking with 8 entry pointsSee also CI_INTEGRATION.md for comprehensive CI/CD integration guide.
npm test # Run all tests (198 tests)
npm test -- --watch # Watch modeTest breakdown:
- 79 baseline tests (v0.1.0 core functionality)
- 27 module-level API tests (getPort, getPorts, PortManager)
- 19 range API tests (reserveRange, getPortInRange)
- 21 edge case tests (corruption recovery, concurrent instances, boundaries)
- 13 resilience tests (stress testing, lifecycle patterns)
portres uses file-based-semaphore-ts (semats) as a library dependency for atomic registry access. This follows PRINCIPLES.md Exception 2 — Tuulbelt tools can compose other Tuulbelt tools since they all have zero external dependencies.
The semaphore ensures that concurrent port allocations from multiple processes never corrupt the registry file, even under high parallelism.
All methods return Result types:
type Result<T> =
| { ok: true; value: T }
| { ok: false; error: Error };Exit codes:
0— Success1— Error (invalid input, no ports available, etc.)
- Port range validation — Prevents allocation outside configured range
- Privileged port protection — Ports < 1024 require explicit
--allow-privileged - Path traversal prevention — Registry paths are validated
- Tag sanitization — Control characters removed from tags
- Registry size limits — Prevents resource exhaustion
- Secure file permissions — Registry files created with mode 0600/0700
This tool integrates with Tuulbelt's dogfooding strategy:
# Validate test reliability (local development)
./scripts/dogfood-flaky.sh 10
# Validate output determinism
./scripts/dogfood-diff.shSee DOGFOODING_STRATEGY.md for the full composition strategy.
▶ View interactive recording on asciinema.org
MIT — see LICENSE
See CONTRIBUTING.md for contribution guidelines.
Part of the Tuulbelt collection:
- File-Based Semaphore (TS) — Cross-platform process locking
- Test Flakiness Detector — Detect unreliable tests
- CLI Progress Reporting — Concurrent-safe progress updates
- More tools at tuulbelt.github.io
