Port Already in Use? The Complete EADDRINUSE Fix Guide (macOS/Linux/Windows)
Symptom: your project won't start, one error line
Error: listen EADDRINUSE: address already in use :::3000
or the friendlier variant:
Port 3000 is already in use
In one sentence: the port you want to listen on is already taken by another process. A dev server, a database, a Docker container, or a leftover node process from a previous run — any of them could be the squatter.
Step one: find who owns the port
macOS / Linux
lsof -ti:3000 # prints just the PID(s) — script friendly
lsof -i:3000 # human form: PID, process name, user
Windows (PowerShell)
netstat -ano | findstr :3000 # last column is the PID
tasklist /fi "PID eq 12345" # resolve the PID to a process name
Step two: deal with the occupant
Case A: your own old dev server (most common)
Last time's Ctrl+C didn't fully clean up, or you closed the terminal while the process lived on. End it:
# macOS / Linux
kill -9 $(lsof -ti:3000)
# Windows (PowerShell)
taskkill /PID 12345 /F
How to tell: the process name from lsof -i:3000 is yours (node, vite, next-server...) → safe to kill.
Case B: another service owns the port
The process isn't your project (postgres, mongod, another team's service) — don't kill it; change your port:
# per-framework port flags
next dev -p 3001 # Next.js
vite --port 3001 # Vite
npm start -- --port 3001 # most CLIs accept it
Case C: a Docker container maps the port
The process name shows docker-proxy:
docker ps --filter "publish=3000" # find the container
docker stop <name> # stop it, or change the compose port mapping
Why kill -9? Isn't plain kill enough?
Try plain kill first (SIGTERM — lets the process exit gracefully); escalate to kill -9 (SIGKILL) only if it survives a few seconds. Leftover dev processes rarely need grace, so -9 is fine; stateful production services (databases) should always get SIGTERM first.
Prevention: never fight over ports again
- Fix a team port convention: 3000/3001/3002 assigned by project — no fighting;
- Use a PORT env var: put
PORT=3001in.env, reference the variable instead of hardcoding; - Exit with Ctrl+C, not by closing the terminal: most dev servers clean up with the session — but background launches (
&/nohup) do not; record PIDs of anything you background; - Port 0 = let the OS pick: tests and scripts listening on port 0 get a free port automatically — conflict eliminated by construction.
FAQ
lsof says port 3000 belongs to ControlCenter on my Mac?
Since macOS Monterey, system services reserve assorted ports (AirPlay Receiver commonly takes 5000/7000). ControlCenter holding 3000 is system behavior — don't kill it; just run your app on another port.
Killed the process but the port is still busy?
Two possibilities: ① the socket sits in TIME_WAIT (auto-releases within a minute — retry shortly); ② a parent supervisor respawned it (Docker restart policy, pm2, systemd) — stop the supervisor instead of repeatedly killing children.
What are the three colons in :::3000?
::: is the IPv6 any-address :: plus the port separator — your service tried to listen on IPv6 port 3000. It's a different socket from IPv4's 0.0.0.0:3000, but most programs dual-stack share them, so treat it as plain occupancy.
One-liner alias
# macOS/Linux: free a port
freeport() { lsof -ti:"$1" | xargs kill -9 2>/dev/null && echo "port $1 freed" || echo "port $1 was free"; }
# usage: freeport 3000
Windows (PowerShell profile):
function freeport($p) { Get-NetTCPConnection -LocalPort $p -ErrorAction SilentlyContinue |
ForEach-Object { Stop-Process -Id $_.OwningProcess -Force } }
The layered diagnostic: socket state before process identity
The error says "address in use" but socket states differ: LISTEN (a process holds it — find and decide), TIME_WAIT (no process — the kernel holds connection slots after a restart; SO_REUSEADDR in the app or wait out the window — the "why won't my server restart" classic), and bound-but-not-listening (a starting application racing your probe). The one-line answers: ss -tlnp shows LISTEN with its owner; ss -tan shows TIME_WAIT floods — the distinction dictates the fix (kill or wait vs configure). Docker adds a layer: docker-proxy holds ports for containers — a conflict with a stopped-but-not-removed container is a prune question, not a process hunt.
IPv4 vs IPv6: two sockets, one error message
:::3000 is IPv6; 0.0.0.0:3000 is IPv4. Many Node frameworks dual-stack, so freeing only one family leaves the other busy. When lsof -iTCP:3000 -sTCP:LISTEN shows two rows (or ss -tlnp | grep 3000 shows both *:3000 and [::]:3000), kill the process once — both sockets usually share a PID. If a tool binds IPv4-only while another grabbed IPv6 (or vice versa), you can get "port free" in one query and EADDRINUSE in the other. Prefer binding explicitly (--hostname 127.0.0.1) in local-only apps to shrink the blast radius.
Windows: excluded port ranges (Hyper-V / WSL)
On Windows, netstat may show nothing listening while bind still fails. Hyper-V and WSL reserve dynamic ranges:
netsh interface ipv4 show excludedportrange protocol=tcp
If your port sits inside an excluded range, no amount of taskkill helps — pick a port outside the range, or temporarily disable the feature that reserved it. This is the Windows cousin of macOS ControlCenter holding 5000/7000.
Framework auto-port vs silent wrong service
Next.js / Vite / Create React App often hop to the next free port when 3000 is taken — and print a one-line warning you miss in a noisy terminal. Symptom: the old server still serves yesterday's build on 3000 while you debug "why isn't my change live?" on 3001. Habit: read the "Local:" URL every start, or set a fixed PORT / -p and fail loud instead of hopping. Pair with the one-liner freeport above when you want the canonical port back.
SO_REUSEADDR is not a free pass
Libraries that set SO_REUSEADDR (and Linux SO_REUSEPORT) can make "port still busy" feel random across restarts. Reuse helps TIME_WAIT restart; it does not let two unrelated processes share a exclusive LISTEN on the same tuple. If two copies of your app start in CI, the second still fails — fix the process lifecycle, do not paper over with reuse flags.
The checklist
lsof -ti:3000(Windows:netstat -ano | findstr :3000) to get the PID- Check the process name: your leftover → kill; someone else's service → move ports; docker-proxy → stop the container
- Still busy after killing: wait out TIME_WAIT, hunt the supervisor, or check Windows excluded ranges / macOS ControlCenter
- Confirm IPv4 and IPv6 both free if the error showed
:::port - Long term: ports in
.env, PIDs for backgrounded processes, port 0 in scripts, fail-loud fixed ports in frameworks that auto-hop
Triage notes
Handling the occupant splits into three cases, keyed off the process name lsof prints: your own leftover process is safe to kill; someone else's service (postgres, another team's daemon) should not be killed — move your own port instead; docker-proxy means stopping the container — one identical error line maps to three completely different actions. 'Why kill -9' stands as its own section because the SIGTERM-then-9 order is what gives stateful services their chance at a graceful exit. The two possibilities behind 'killed it and the port is still busy' (TIME_WAIT releasing on its own within a minute; a parent process pulling up new children) unfold from the same logic: without finding the supervisor, repeatedly killing child processes is pointless.
Provided by ToolVault. Related tools: Dockerfile Generator, Run to Compose, Linux Cheatsheet, HTTP Status Codes. Related reading: ECONNREFUSED troubleshooting, Docker port is already allocated. See the homepage for more developer tools.
Related Tools
Related Articles
Linux Crontab in Practice: Commands, Logs, and Debugging
Crontab beyond syntax: three must-know commands, /etc/cron.d vs user crontabs, five-step debugging, PATH/timezone/% escaping, MAILTO, and what to do when you need second-level scheduling.
docker run to docker-compose: The Complete Migration Guide (with Flag Map)
Migrate docker run commands to docker-compose.yml: five advantages of compose over raw run, a flag-by-flag mapping table (ports, volumes, env, restart, networks), the classic pitfalls (bare -e passthrough, anonymous volumes, --rm semantics), and an online converter that runs locally in your browser.
Permission denied (publickey): 6 Reasons Git Push Fails Over SSH (and the Fix for Each)
git clone or push rejected with Permission denied (publickey) fatal: Could not read from remote repository? Covers missing keygen, key not loaded in the agent, public key not added to GitHub/GitLab, multi-account key routing with ~/.ssh/config, deploy key limits, and wrong remote URLs — with ssh -v diagnostics.