EADDRINUSE again? This tool tells you who owns the port before you kill anything

port-reclaim finds 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.

port-reclaim freeing a port, declining to kill a database, and refusing a Docker process

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:

  1. -9 skips graceful shutdown — anything mid-write to a socket, DB, or temp file gets truncated.
  2. 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.
  3. 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, etcd always 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, lsof returns 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 exits 1, so port-reclaim 3000 && next dev fails 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 to process.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.