v1.0 — Now available for macOS · Press ⌘ Space to launch
← Back to blog

What is using port 3000 on my Mac? A practical investigation

CmdSpace Team·

You ran npm run dev and got "Error: listen EADDRINUSE: address already in use :::3000". Your terminal is fresh, you don't remember leaving anything running, and Activity Monitor shows nothing obvious. The port is in use. By what? This post…

You ran npm run dev and got "Error: listen EADDRINUSE: address already in use :::3000". Your terminal is fresh, you don't remember leaving anything running, and Activity Monitor shows nothing obvious. The port is in use. By what? This post is the practical investigation — every angle to check, in the order I check them when this happens to me.

If you skim: lsof -ti :3000 gives you the PID, ps -p <pid> tells you what it is. The rest of this post is for cases where those two don't immediately solve it.

Step 1: ask lsof

lsof -i :3000

Output looks like:

COMMAND  PID   USER FD   TYPE DEVICE  NODE NAME
node    51234 you  20u  IPv4 0x...    TCP  *:3000 (LISTEN)

The PID column (51234 in this example) is what you want. Now ask ps:

ps -p 51234 -o pid,ppid,command

Output:

  PID  PPID COMMAND
51234 51200 node /path/to/dev-server.js

You now know exactly what is using port 3000 and where its parent is. This step solves the issue 90% of the time. The rest of this post is for the other 10%.

For the syntax cheatsheet, see the lsof port cheatsheet for macOS.

Step 2: when lsof returns nothing

Sometimes lsof -i :3000 returns no rows but the port is still rejected as "in use." Three explanations:

a) Permission

Some processes — especially system services — are only visible under sudo:

sudo lsof -i :3000

If sudo lsof shows a row, that is your culprit and you cannot kill it without elevated privileges.

b) TIME_WAIT lingering

A previous connection on port 3000 closed but the kernel is still holding the socket in TIME_WAIT state. Default macOS TIME_WAIT is 15-30 seconds. The error you see is technically "address in use" because the kernel has not released the socket yet. Two options:

  • Wait 30 seconds and retry. Almost always solves it.
  • Reuse the address in your app. Most Node servers default to SO_REUSEADDR already; if yours does not, enable it.

c) Docker container with a published port

If you have a running Docker container that published port 3000, lsof on the host might not show it directly because the docker-proxy process is what holds the socket. Check:

docker ps | grep 3000

If a container is published on 3000, stop it:

docker stop <container-id>

This is the cause of about half of the "lsof shows nothing but port is busy" reports I see.

Step 3: when ps gives you a cryptic command

ps -p <pid> sometimes returns something useless like /Library/Containers/.../somebinary. To find out more:

a) Parent process tree

ps -p <pid> -o pid,ppid,command
# then look up the parent:
ps -p <ppid> -o pid,ppid,command
# and so on up the tree

The grandparent is often more informative than the immediate parent (e.g. "Visual Studio Code" rather than "node").

b) What binary is actually executing

ls -la /proc/<pid>/exe   # NOT available on macOS

macOS does not have /proc. The closest is:

lsof -p <pid> | head

This shows the open files for the process, including the executable. The line marked cwd is the working directory; the txt line is the binary path.

c) When the process is gone by the time you look

Sometimes the PID has already exited between lsof and ps. If you suspect this, capture both in one shot:

lsof -i :3000 -F pcL

-F is "field output" — machine-readable. pcLi includes PID, command, login name. Faster than two separate calls.

Step 4: common culprits in 2026

In rough order of frequency for macOS developers:

a) A dev server you forgot was running

Closed the terminal tab, did not stop the server first. Node servers especially tend to outlive their terminal tabs if they were started with nohup, &, or a process manager.

Fix: lsof -ti :3000 | xargs kill.

b) Docker Desktop

Running container with a port binding. Easy to forget.

Fix: docker ps to find it, docker stop <id> to free the port.

c) A globally installed dev tool

json-server, http-server, static-server, or similar tools sometimes spawn detached background processes. Check:

pgrep -f json-server
pgrep -f http-server

Fix: pkill -f <name> or kill <pid>.

d) Apple's own services

System services like controlcenter, airportd, cfprefsd can bind ports. Almost always on non-developer ports (mDNS on 5353, NetBIOS on 137, etc.), but worth checking with sudo lsof.

e) VPN or proxy software

Cisco AnyConnect, Tailscale, Cloudflare WARP, and similar tools bind various local ports for their proxy layers. If your dev port matches a VPN's listener, the conflict is one-time and rare.

f) Old IDE child processes

Visual Studio Code's integrated terminal can leak child processes when the IDE crashes. Look for electron or VS Code subprocess PIDs in ps.

Step 5: when none of the above work

Three escalations:

a) Restart Docker

killall Docker
open -a Docker

Sometimes Docker's bookkeeping gets out of sync with reality.

b) Check launchd-managed services

sudo launchctl list | grep -v "^-"

Look for anything that might bind a network port. Most launchd services do not, but a few do.

c) Restart your Mac

Always works. Last resort. If you find yourself doing this monthly, you have a process-management problem upstream.

The investigation cheatsheet

# 1. Who is on the port?
lsof -i :3000

# 2. What does the PID actually do?
ps -p <pid> -o pid,ppid,command

# 3. Free the port
lsof -ti :3000 | xargs kill

# 4. Verify it worked
lsof -i :3000   # should return nothing

# 5. If step 1 returned nothing, try sudo
sudo lsof -i :3000

# 6. If still nothing, check Docker
docker ps | grep 3000

Six commands cover 95% of investigations.

A launcher-integrated alternative

If you investigate port conflicts more than once a week, the ergonomics of the command-line dance get old. A launcher with a built-in "show what's on port X" command compresses steps 1 and 2 into one keystroke pattern.

CmdSpace has a "port lookup" command that runs lsof -i :PORT + ps -p + displays a single row with the process name. Press hotkey, type "port 3000", see the answer. The full kill-by-port flow is covered in kill a process by port on macOS.

When a port conflict is actually a misconfiguration

Sometimes the port conflict is not "another process is using this port" but "your service is trying to bind to a port that does not match your config." Symptoms:

  • lsof -i :3000 shows your own process listening.
  • You restart your service, it fails with "in use," but you know nothing else is on 3000.

Cause: your service is starting two listeners on the same port (e.g. WebSocket and HTTP both trying to bind 3000) or a hot-reload loop is creating zombies. Check your service config, not the port.

The bottom line

lsof -i :PORT is the first command. ps -p <pid> is the second. lsof -ti :PORT | xargs kill is the cure. Everything else on this page is for the corner cases.

Related reads: lsof port cheatsheet for the syntax reference, kill a process by port on macOS for the full method ranking.


Sources