Skip to content
DevOps2026-09-076 min read

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

  1. Fix a team port convention: 3000/3001/3002 assigned by project — no fighting;
  2. Use a PORT env var: put PORT=3001 in .env, reference the variable instead of hardcoding;
  3. 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;
  4. 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

  1. lsof -ti:3000 (Windows: netstat -ano | findstr :3000) to get the PID
  2. Check the process name: your leftover → kill; someone else's service → move ports; docker-proxy → stop the container
  3. Still busy after killing: wait out TIME_WAIT, hunt the supervisor, or check Windows excluded ranges / macOS ControlCenter
  4. Confirm IPv4 and IPv6 both free if the error showed :::port
  5. 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.