EADDRINUSE: Finding and Killing Whatever Took Your Port
Address already in use. Here is how to find the process, why it sometimes is not a process at all, and why the port stays busy after you kill it.
The short answer
Error: listen EADDRINUSE: address already in use :::3000
Find what holds the port and kill it:
# macOS and Linux
lsof -i :3000
kill -9 $(lsof -t -i:3000)
# Linux, more detail
ss -lptn 'sport = :3000'
# Windows
netstat -ano | findstr :3000
taskkill /PID <pid> /F
If nothing is listed but the port is still refused, you are hitting TIME_WAIT and you want SO_REUSEADDR, covered below.
Tested on Node 22.14, Linux 6.8, macOS 15.
Finding the holder
lsof is the most direct tool:
$ lsof -i :3000
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node 48213 rohit 21u IPv6 0x9f2a 0t0 TCP *:3000 (LISTEN)
On Linux ss is faster and gives you more:
$ ss -lptn 'sport = :3000'
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 511 *:3000 *:* users:(("node",pid=48213,fd=21))
Note Recv-Q and Send-Q. A large Recv-Q on a listening socket means the accept backlog is filling up, which is a different problem worth noticing while you are here.
If ss shows nothing and you are root, check for a process in another network namespace. Containers have their own, so a port bound inside a container will not appear in the host's ss output unless the port is published.
sudo ss -lptn | grep 3000 # host namespace
docker ps --format '{{.Names}}\t{{.Ports}}' # published container ports
Why it is still in use after you killed the process
This is the confusing case. You killed the process, lsof shows nothing, and binding still fails.
TCP requires that a socket which has actively closed a connection sits in TIME_WAIT for twice the maximum segment lifetime, typically 60 seconds on Linux. This exists so that delayed packets from the old connection do not get delivered to a new one that happens to reuse the same four tuple.
By default, a bind fails if any socket on that port is in TIME_WAIT. The fix is SO_REUSEADDR, which tells the kernel to allow binding despite lingering TIME_WAIT sockets:
// Node sets SO_REUSEADDR by default on server.listen()
// so if you hit this in Node, it is usually a real process
Node already sets it. Python does not by default:
import socket
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # add this
s.bind(("0.0.0.0", 3000))
Go's net.Listen sets it. Rust's TcpListener::bind does not, and you need socket2 to set it manually.
Check for lingering sockets:
ss -tan | grep 3000
# TIME-WAIT 0 0 10.0.0.5:3000 10.0.0.9:52344
Do not fix this by lowering tcp_fin_timeout or enabling tcp_tw_recycle. The latter was removed from Linux 4.12 because it broke connections from clients behind NAT, and you will still find blog posts recommending it.
Why it fails only sometimes
A previous instance did not die. Nodemon, tsx watch, and similar tools occasionally leave orphans when a reload races with a crash. The classic symptom is that it works after a reboot and fails again within a day.
ps aux | grep node | grep -v grep
pkill -f "node.*server.js"
The port is in the ephemeral range. Linux assigns outbound connection source ports from net.ipv4.ip_local_port_range, which defaults to 32768 to 60999. If your service listens on a port inside that range, an outbound connection may have grabbed it first, and it will fail intermittently and unreproducibly.
cat /proc/sys/net/ipv4/ip_local_port_range
# 32768 60999
If your service listens on, say, 40000, move it below 32768 or reserve it:
sysctl -w net.ipv4.ip_local_reserved_ports=40000
This one is genuinely nasty because it fails maybe one start in fifty, and everyone assumes it is a leftover process.
IPv4 and IPv6 dual binding. The error :::3000 means IPv6 wildcard. On Linux, binding :: also binds 0.0.0.0 by default, controlled by net.ipv6.bindv6only. So a process bound to IPv6 wildcard blocks an IPv4 bind on the same port, and the two processes look unrelated in your output.
Docker holds it. A stopped container with a published port sometimes leaves docker-proxy running:
ps aux | grep docker-proxy
docker ps -a --filter "publish=3000"
In containers and Kubernetes
Inside a container, EADDRINUSE almost always means your own process, since the network namespace is private. Two processes in one container, usually because an entrypoint script started the app and something restarted it.
In Kubernetes, if you use hostPort or hostNetwork, you are back in the host namespace and competing with everything else on the node. A pod that schedules on one node and not another, with EADDRINUSE in the logs, is nearly always hostPort contention. Check with:
kubectl get pods --all-namespaces -o json \
| jq -r '.items[] | select(.spec.containers[].ports[]?.hostPort==3000) | .metadata.name'
This shows up as a pod stuck in CrashLoopBackOff with exit code 1, and the log line is the only clue.
Handling it properly in code
Crashing on EADDRINUSE is correct behaviour. Do not retry silently in a loop, because you will end up with two instances when the other one exits.
Do produce a message somebody can act on:
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(
`Port ${PORT} is already in use.\n` +
`Find it with: lsof -i :${PORT}\n` +
`Or set PORT to something else.`
);
process.exit(1);
}
throw err;
});
That is thirty seconds of work and it converts a stack trace into an instruction. It is the same argument I made about error messages being a user interface: the program knows the port number, so it should say it.
Shut down cleanly so the socket is released promptly:
const shutdown = () => {
server.close(() => process.exit(0));
setTimeout(() => process.exit(1), 10000).unref();
};
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
The SIGTERM handler matters in containers. If your process ignores SIGTERM, the orchestrator waits out the grace period and then SIGKILLs it, which leaves connections in a worse state and shows up as exit code 143 or 137.
Prevention
Use a port allocator in tests. Hardcoded ports in a test suite fail the moment tests run in parallel. Binding to port 0 makes the kernel assign a free one:
const server = app.listen(0, () => {
const { port } = server.address();
});
Pick ports below 32768 for services, so you never collide with the ephemeral range.
One process per container. If your entrypoint starts more than one thing, use a supervisor that manages both rather than backgrounding with &.
Print the port on startup. listening on http://localhost:3000 in the log means the next person does not have to read the config to know what to check.