DEV Community

Thomas Shafer
Thomas Shafer

Posted on

EADDRINUSE on macOS: find what holds the port, and stop it safely

You run npm run dev and get this:

Error: listen EADDRINUSE: address already in use :::3000
Enter fullscreen mode Exit fullscreen mode

Something is already listening on port 3000. Usually it's a dev server you forgot about: a terminal tab you closed, an editor task that never exited, or a watcher that outlived its parent. This post covers how to find the owner on macOS, how to stop it without guessing, and a pair of shell functions so you only have to work it out once.

Step 1: ask the kernel who is listening

lsof is already on your Mac. This is the form worth memorising:

lsof -nP -iTCP:3000 -sTCP:LISTEN
Enter fullscreen mode Exit fullscreen mode

What each flag does:

  • -n skips DNS lookups, so it returns instantly.
  • -P shows 3000 instead of a service name.
  • -iTCP:3000 limits it to TCP port 3000.
  • -sTCP:LISTEN hides client connections, so you only see the process that owns the port.

Example output:

COMMAND   PID USER   FD   TYPE  DEVICE SIZE/OFF NODE NAME
node    48213 tom    23u  IPv6  0x1234      0t0  TCP *:3000 (LISTEN)
Enter fullscreen mode Exit fullscreen mode

The -sTCP:LISTEN filter matters more than it looks. Without it, lsof -i :3000 also lists your browser's open connections to the server, and you can end up killing Chrome instead.

If you only want the PID, -t prints just that:

lsof -nP -tiTCP:3000 -sTCP:LISTEN
Enter fullscreen mode Exit fullscreen mode

Step 2: check what it is before you kill it

A PID tells you very little. Before you stop it, look at two things: the full command line and the working directory.

ps -o pid,etime,command -p 48213
lsof -a -p 48213 -d cwd -Fn | sed -n 's/^n//p'
Enter fullscreen mode Exit fullscreen mode

etime shows how long it has been running, and the cwd line shows which project folder it was started from. Together they usually settle it. A node process that has been up for three days, started from ~/old-prototype, is safe to stop. One that started two minutes ago from the project you're working on is probably your own server, running in another tab.

Step 3: ask nicely first

kill with no signal sends SIGTERM, which lets the process close its sockets and clean up. Use kill -9 only if that doesn't work.

kill 48213
sleep 1
lsof -nP -tiTCP:3000 -sTCP:LISTEN || echo "port 3000 is free"
Enter fullscreen mode Exit fullscreen mode

Reaching for kill -9 first is a common habit and it costs you something real. A process killed that way can't remove its lock files, flush its logs or shut down its child processes, and you can end up with a half-written database or a leaked worker.

Step 4: put it in your dotfiles

Here are two functions for ~/.zshrc. port shows who holds a port. killport stops it with SIGTERM, waits, and only then falls back to SIGKILL.

port() {
  lsof -nP -iTCP:"${1:?usage: port N}" -sTCP:LISTEN
}

killport() {
  local pids
  pids=$(lsof -nP -tiTCP:"${1:?usage: killport N}" -sTCP:LISTEN)
  [ -z "$pids" ] && { echo "nothing is listening on $1"; return 0; }
  echo "$pids" | xargs kill
  sleep 1
  pids=$(lsof -nP -tiTCP:"$1" -sTCP:LISTEN)
  [ -n "$pids" ] && echo "$pids" | xargs kill -9
  lsof -nP -tiTCP:"$1" -sTCP:LISTEN >/dev/null || echo "port $1 is free"
}
Enter fullscreen mode Exit fullscreen mode

Open a new terminal and run port 3000 or killport 3000.

One limit: this only sees processes you own. If the listener belongs to root or another user, lsof without sudo may show nothing at all. In that case you'll get "nothing is listening" even though the port really is taken, so try sudo lsof ... before concluding it's free.

When killing the process isn't the answer

Some holders come back, or aren't what they look like.

Port 5000 or 7000 on a recent Mac. macOS's AirPlay Receiver listens on both. The owner shows up as ControlCe (Control Center). Turn it off in System Settings > General > AirDrop & Handoff > AirPlay Receiver, or move your dev server to another port. Killing it just makes macOS start it again.

The owner is Docker. If lsof shows com.docker.backend or similar, a container has published the port. Use docker ps --format '{{.Names}}\t{{.Ports}}' to find the container, then docker stop it. Killing the backend process takes down every container.

The process has a parent that restarts it. nodemon, next dev, a docker compose restart policy and some editor tasks will respawn what you kill. Check the parent with ps -o ppid= -p PID, then look at what that parent is.

Nothing is listening, but the bind still fails. This is rare. It can happen when a server binds the same address and port twice, such as localhost resolving to both ::1 and 127.0.0.1, or two copies of a framework starting in one process. lsof will show one listener for each address family, so read the NAME column carefully.

A small habit that prevents most of this

Most EADDRINUSE errors come from servers that were started and then abandoned. Two habits cut them down:

  1. Run dev servers in the foreground in a terminal you can see. Don't background them with &.
  2. Let your tooling pick a free port where it supports that. Many frameworks will move to the next port if you tell them to, instead of failing.

Full disclosure: I made a tool for this

I built Binnacle, a native Mac menu bar monitor, partly because I got tired of typing the commands above. In its ⌘K command line, port 3000 shows the process, and kill port 3000 stops it after a confirmation. Its Projects page groups dev servers by project folder and shows their ports, memory and how long they've been idle, with a Stop button. The menu bar readouts are free. The rest of the app is free for 14 days, then $9 once. It needs macOS 14 or later. It's at https://binnacle.shafer.llc?utm_source=devto.

You don't need it for any of the above. The two functions in your .zshrc will cover most days.

What's the strangest thing you've found sitting on a port? I'm curious what other surprise listeners are out there.

Top comments (0)