Fix "port already in use" (EADDRINUSE) on macOS
Your dev server crashed, you restarted it, and now it greets you with Error: listen EADDRINUSE: address already in use :::3000. The good news: this is almost always cosmetic. Here's why it happens, the safe way to free the port, and worked examples for the stacks where it bites most.
What EADDRINUSE actually means
EADDRINUSE is the POSIX error the kernel returns when a process calls bind() on a TCP/UDP address-port pair that's already claimed. It is never a bug in your app's logic — it's the OS refusing to double-book the port. Two scenarios produce it:
- Another process is genuinely listening on the port.
- The previous instance of your process exited, but the kernel still considers the socket in use (typically
TIME_WAIT).
The fix is different in each case.
Step 1: Find out who's holding the port
The fastest answer is lsof:
lsof -nP -iTCP:3000 -sTCP:LISTEN
Three outcomes:
- One row, your process name (
node,ruby,python…). A previous run is still alive. Go to step 2. - One row, an unfamiliar process. Something else is on the port. Either change ports or stop that process.
- No output. Nothing is listening — but you still got
EADDRINUSE. That'sTIME_WAIT. Go to step 3.
(For a fuller walkthrough of lsof, see finding what process is using a port on macOS.)
Step 2: Free the port — gracefully
If the offender is your own process, kill it with SIGTERM first:
kill $(lsof -ti :3000)
SIGTERM is the polite signal. The process catches it, closes its sockets, flushes any pending writes, removes its PID file, and exits. That's what you want.
If after a couple of seconds lsof -ti :3000 still returns the same PID, escalate:
kill -9 $(lsof -ti :3000)
SIGKILL is unstoppable — the process never sees it. That's why it's a last resort: anything the process should have cleaned up (lock files, temp dirs, sockets in CLOSE_WAIT) gets left behind.
Don'tsudo kill -9by reflex. If your shell can't kill the process, almost always the right move is to find out why — usually because it's running under a different user. Addingsudomakes the cleanup problem worse.
Step 3: When nothing's listening but the port's still busy
You see EADDRINUSE, but lsof shows nothing. This is TIME_WAIT: the TCP closing handshake leaves a socket lingering for 30–120 seconds so any delayed packets can be matched and dropped, not delivered to a new server on the same port.
You have three options:
- Wait it out. Two minutes max, usually less.
- Tell your server to set
SO_REUSEADDR. This lets the kernel reuse aTIME_WAITsocket. Most frameworks set it by default. If yours doesn't, add it to your bind call. - Bind to a different port. Often the easiest path during development.
You can confirm the state with netstat:
netstat -anv -p tcp | grep 3000
If you see lines ending in TIME_WAIT, that's your culprit.
Worked examples
Node / Express / Next.js / Vite
Error: listen EADDRINUSE: address already in use :::3000
Cause: nodemon, npm run dev, or a tab in another terminal still holds the port. Fix:
lsof -ti :3000 | xargs kill
If you're on Next.js or Vite, you can also pass a different port: PORT=3001 npm run dev or vite --port 5174.
Rails / Puma
A server is already running. Check /tmp/pids/server.pid
Rails refuses to start if the PID file exists. Two fixes:
# 1. The actual process is dead, just stale PID file
rm tmp/pids/server.pid
# 2. The actual process is alive, kill it first
kill $(cat tmp/pids/server.pid)
rm tmp/pids/server.pid
Python / Flask / Django
OSError: [Errno 48] Address already in use
Same pattern: lsof -ti :8000 | xargs kill. Django's runserver normally sets SO_REUSEADDR, so this is usually a real lingering process — not TIME_WAIT.
Docker
Error: Bind for 0.0.0.0:5432 failed: port is already allocated
Two flavors:
- A previous container still has it.
docker ps, thendocker stop <name>. - A host process has it. A native Postgres install often binds
5432.lsof -i :5432will tell you. Either stop the host service (brew services stop postgresql) or remap the container's host port (5433:5432in docker-compose).
Prevention: leave fewer ghosts behind
- Use a process manager during dev (foreman, overmind, pm2). They reliably reap their children on Ctrl+C.
- Set
SO_REUSEADDRin custom socket code. Express, Rails, Django, FastAPI all do this for you; hand-rolled servers often don't. - Trap signals in production. A server that handles SIGTERM never leaves
TIME_WAITgarbage — it does an orderly close. - Keep an eye on the menu bar. A live view of listening ports makes "wait, didn't I kill that?" impossible.