Manfath / Blog / EADDRINUSE on macOS

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:

  1. Another process is genuinely listening on the port.
  2. 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:

(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't sudo kill -9 by 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. Adding sudo makes 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:

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.

Free a port without typing.
Manfath shows every listening port in your menu bar with one-click kill — SIGTERM first, SIGKILL only if it has to. Free, open-source, no telemetry.

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:

Prevention: leave fewer ghosts behind


Read next