> ## Content Index
> Fetch the complete content index at: https://debugly.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# EADDRINUSE: Finding and Killing Whatever Took Your Port
- URL: https://debugly.dev/eaddrinuse-port-already-in-use-fix/
- Published: 2025-11-04T08:50:00.000Z
- Updated: 2026-10-17T05:59:08.000Z
- Description: 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.
- Author: Rohit Bhadani
- Tags: Node.js, Error Autopsy, DevOps, Networking

## The short version

```
Error: listen EADDRINUSE: address already in use :::3000

```

Find what holds the port and kill it:

```bash
# 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:

```bash
$ 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:

```bash
$ 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.

```bash
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:

```javascript
// 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:

```python
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:

```bash
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.

```bash
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.

```bash
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:

```bash
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:

```bash
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:

```bash
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](https://debugly.dev/crashloopbackoff-kubernetes-diagnostic-guide/), same mechanism, different costume.

## 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:

```javascript
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](https://debugly.dev/error-messages-are-a-user-interface/): the program knows the port number, so it should say it.

Shut down cleanly so the socket is released promptly:

```javascript
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](https://debugly.dev/oomkilled-container-memory-limit-debugging/).

## 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:

```javascript
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.

---

Published on [debugly.dev](https://debugly.dev/) by [Rohit Bhadani](https://debugly.dev/about/) | Founder of [Krova Cloud](https://krova.cloud/?ref=debugly.dev)