What is using port 3000 on my Mac? A practical investigation
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_REUSEADDRalready; 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 :3000shows 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
- Stack Overflow canonical "kill by port" answer — stackoverflow.com
- kill-port npm package — github.com/kill-port/kill-port
- npm port-in-use thread — github.com/npm/npm
- yarn port-in-use thread — github.com/yarnpkg/yarn