EADDRINUSE again? This tool tells you who owns the port before you kill anything
port-reclaimfinds the process holding a busy dev port, shows you what it is, reclaims it when it's obviously yours, asks when it isn't, and refuses when it would be a mistake.
| Try it now | npx port-reclaim 3000 |
| Platforms | macOS · Linux · Windows · WSL2 (PowerShell + CMD) |
| Requires | Node.js 18+ |
| Runtime deps | 1 |
| Package size | ~21 KB |
| Telemetry | none — no network calls, nothing stored |
| License | MIT |
Why this exists
Error: listen EADDRINUSE: address already in use :::3000 The reflex is lsof -i :3000 → read a PID → kill -9 it. Three things go wrong with that:
-
-9skips graceful shutdown — anything mid-write to a socket, DB, or temp file gets truncated. - You're typing a number you read off a screen into a destructive command — TCP and UDP both appear, PIDs repeat, and fatigue picks the wrong one.
- A PID is not a decision. "Something is on 3000" ≠ "that's the dev server you orphaned 40 minutes ago, not the Postgres."
The one check that fixes it
port-reclaim compares the working directory of the process with the directory you're standing in.
$ port-reclaim 3000 --list Port 3000 is used by 'node' (PID 21836, '~/project', up 42m 11s) — matches the current directory. $ port-reclaim 3000 Released port 3000 from node (PID 21836). | Situation | What happens |
|---|---|
| Process started from your directory | Reclaimed silently — graceful signal first, forceful only if it survives |
| Different directory, or CWD unreadable | Shows name, PID, path, uptime — asks you |
Docker, HTTP.sys, systemd | Refuses, even with --yes |
| A database | Always asks, even if it shares your directory |
| Owner invisible to your user | Reports it as unclaimable instead of "free" |
| Windows process, you're in WSL2 | Names it, and explains why Linux can't see it |
$ port-reclaim 5432 Port 5432 is used by 'postgres' (PID 3352, '~/dbdata', up 3h 12m). Kill it? (y/N) What it refuses to touch
The refusals are the point. They exist because the easy version of this tool is 20 lines of shell.
Docker & the kernel are off-limits.
Port 8080 is used by 'docker-proxy' (PID 16004, …). Refusing to kill it — it appears to be a Docker process — stop the container instead (for example 'docker stop <container>').
Killing docker-proxy doesn't stop your container, it just leaves Compose in a worse state. PIDs 0–4 are protected on the same logic: System/HTTP.sys on Windows, systemd on Linux. --yes cannot override either.
Databases never die quietly.
postgres,postmaster,mysqld,mariadbd,mongod,redis-server,memcached,etcdalways confirm — even when running from inside your project, which is exactly the case a same-directory shortcut gets catastrophically wrong."I found nothing" is not "the port is free." When the owner is another user's or an elevated process,
lsofreturns nothing — and empty output looks identical to a free port. So the tool checks the answer by briefly binding the port:
Port 3000 is in use by a process port-reclaim cannot see — it may belong to another user, an elevated process, or a socket still closing.
It exits1, soport-reclaim 3000 && next devfails loudly instead of succeeding at nothing.WSL2 tells the truth. Under default NAT networking, Linux and Windows have separate port spaces, so a Windows-held port looks free to every Linux tool — including this one until I tested it. It reaches across and says:
Port 4951 is free inside WSL, but a Windows tcp listener 'node.exe' (PID 19220) already holds it.
It reports rather than acts: a Windows PID passed toprocess.kill()would signal an unrelated Linux process.
Made for scripts
port-reclaim 3000 5173 8080 # several ports port-reclaim --match '^node$' # every port held by a matching process name port-reclaim --json # machine-readable, never terminates port-reclaim # ports from config, no arguments { "port-reclaim": { "ports": [3000, 5173], "ignore": [5432] } } A .reclaimignore file protects ports absolutely — that beats every heuristic. --list inspects without acting. Exit codes: 0 free or released, 1 still in use, 2 bad input. Colour turns itself off when output isn't a terminal (--no-color, NO_COLOR, FORCE_COLOR all respected — that's how the GIF above was recorded). There's a programmatic API too: discover, discoverListening, terminate, probe.
Fast, and measured
Whole-command wall clock on Windows 11 under Node 24, including Node's own startup, because that's the part people leave out of benchmarks:
| before | now | |
|---|---|---|
| answer "is port 3000 free?" | ~1.00s | ~0.10s |
| identify what's on a busy port | ~1.26s | ~0.41s |
| full reclaim of one process | — | ~0.59s |
The "before" was a bug: discovery started PowerShell three times per port, and a cold powershell.exe costs ~0.7s. Now netstat decides whether anything is on the port, and PowerShell starts at most once, to name the processes actually found.
The honest limits
- Not a process manager or supervisor, and no substitute for
docker compose down. - UDP matching is a heuristic — a UDP socket on a port can belong to a client, not a server.
- The free-port check binds momentarily, so a connection arriving in that instant can read as "in use". A race, not a flaw I'll paper over.
- It can't see other users' processes without your own elevated privileges.
-
0.x, one maintainer, ~52 tests across macOS/Linux/Windows × Node 18/20/22/24. The programmatic API may still move — read the changelog before upgrading.
Five real bugs are documented in that changelog — a taskkill missing /F that reported success while the port stayed busy, a PowerShell cmdlet name that never existed, an uptime calculation always negative on Windows, process names silently wrong on macOS and Node 24 (which quietly disabled the protections above), and a WSL check that once blamed WSL's own relay process. All five are now covered by tests that bind a real socket and make the tool find it through the actual OS.
Get it
npx port-reclaim 3000 # or npm install --global port-reclaim npm → port-reclaim · source → princegoel0/port-reclaim
If it saves you one bad kill -9, it did its job. OS quirks I haven't hit are exactly what I want an issue about.
