Emmanuel Corels
← Blog
Networking & ISP Systems

Free port 3000 on macOS without killing the wrong process

Identify the listening socket, trace its process and parent, stop it gracefully, then prove the port is genuinely free before restarting your service.

By Emmanuel Corels

Your development server exits with EADDRINUSE: something already owns TCP port 3000. The fastest unsafe response is to paste a force-kill one-liner. The reliable response is to identify the listening socket, establish what launched it, ask it to stop cleanly, and verify the result.

Query the listener, not every connection involving the port

lsof -nP -iTCP:3000 -sTCP:LISTEN

-iTCP:3000 selects TCP activity for that port; -sTCP:LISTEN narrows it to listeners; -n avoids DNS lookups; and -P keeps numeric ports rather than translating them to service names. A typical result includes the command, PID, user, file descriptor, address family and listening address.

If nothing appears, retry with privileges because macOS may restrict visibility into another user’s processes:

sudo lsof -nP -iTCP:3000 -sTCP:LISTEN

An empty result can also mean the original error involved UDP, IPv6-only timing, a process that already exited, or a supervisor rapidly restarting the listener.

Confirm the identity before sending a signal

Assume the listener PID is 51432:

ps -p 51432 -o pid=,ppid=,user=,etime=,command=
lsof -a -p 51432 -d cwd
ps -p "$(ps -p 51432 -o ppid= | tr -d ' ')"   -o pid=,ppid=,command=

The working directory and parent process often distinguish an old Node development server from a package manager, test runner, container runtime or launch agent. If the parent is a supervisor, killing only the child may make the port reappear immediately.

Stop through the owning tool when possible

Prefer the application’s normal control path: stop the foreground terminal with Control-C, use the package runner’s stop command, stop the relevant Compose service, or unload the correct launch agent. If no managed stop path exists, request graceful termination:

kill -TERM 51432

SIGTERM gives the process an opportunity to close sockets and flush work. Wait briefly and verify:

lsof -nP -iTCP:3000 -sTCP:LISTEN
ps -p 51432 -o pid=,state=,command=

Only if the process is confirmed stuck and the consequences are acceptable should you escalate:

kill -KILL 51432

SIGKILL cannot be handled by the process. It may leave temporary files, interrupted writes or stale external state, so it is not a convenient synonym for “stop.”

A safe shell sequence that resists stale PIDs

Between discovery and termination, a short-lived process can exit and macOS can eventually reuse its PID. Keep inspection and action visibly separate:

pid="$(lsof -nP -t -iTCP:3000 -sTCP:LISTEN)"
printf 'Listener PID: %s
' "$pid"
ps -p "$pid" -o pid=,ppid=,user=,etime=,command=

# After reviewing the output:
kill -TERM "$pid"

If multiple PIDs are returned, do not feed the multiline value blindly into a command. Inspect each listener and determine why more than one socket is present. Address reuse, IPv4/IPv6 listeners and supervisors can complicate the picture.

When port 3000 becomes occupied again

while true; do
  date
  lsof -nP -iTCP:3000 -sTCP:LISTEN
  sleep 1
done

A recurring PID with a stable parent suggests a managed service. Investigate the parent and the relevant control system rather than repeatedly killing children. Common owners include launchd, a process manager, a container, an IDE task or a test watcher.

Prove the replacement service is the one answering

npm run dev
lsof -nP -iTCP:3000 -sTCP:LISTEN
curl -sv http://127.0.0.1:3000/health

A free port is only the midpoint. Record the new PID and command, then test an application route. If the service listens on ::1, 127.0.0.1, 0.0.0.0 or *, its reachability differs; verify the address matches the intended exposure.

ObservationMeaningNext move
No listener, but bind still failsRace, other protocol/address family, or restricted visibilityRetry with sudo and inspect immediately during failure
PID returns after TERMSupervisor or wrapper restarted itTrace PPID and stop through owner
Listener is another userShared workstation or system serviceDo not kill until ownership and impact are confirmed
New app starts but curl failsPort ownership solved; app path still unhealthyInspect binding, logs and health route

A PID is a locator, not an identity. Verify the command, user, parent and working directory before treating it as the process you meant to stop.

Sources: the solved Stack Overflow discussion (CC BY-SA), the macOS lsof reference, and the local macOS manual pages available through man lsof, man ps and man kill.

Primary source: Review the official reference ↗