Starting a Node server when something else already holds the port produces this:
Error: listen EADDRINUSE: address already in use :::3000
at Server.setupListenHandle [as _listen2] (node:net:2008:16)
at listenInCluster (node:net:2065:12)
at Server.listen (node:net:2170:7)
Unhandled, it takes the process down with it:
node:events:487
throw er; // Unhandled 'error' event
^
This is an operating-system refusal, not a Node bug. The kernel will not give the same address-and-port combination to two processes, so no amount of restarting will help until whatever holds port 3000 releases it. The :::3000 is the IPv6 wildcard address — Node binds all interfaces when you do not name a host.
Find the culprit first:
lsof -i :3000
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node 41288 you 23u IPv6 0x8a2f 0t0 TCP *:hbci (LISTEN)
Then stop it:
kill $(lsof -ti :3000)
Why This Happens
Work down these in order — the first two cover most cases.
| Cause | Tell |
|---|---|
| 1. A previous run of your own app is still alive | lsof shows node, often more than one |
| 2. A different application owns the port | lsof shows something else entirely |
| 3. Your code binds the same port twice | Only one process in lsof, and it is yours |
| 4. A container publishes the port | lsof shows com.docke or nothing useful |
5. The socket is in TIME_WAIT | No process listed; clears on its own |
Fix 1: Find and Stop the Process
macOS and Linux:
# What is on the port?
lsof -i :3000
# Just the PID
lsof -ti :3000
# Stop it politely (SIGTERM — lets it clean up)
kill $(lsof -ti :3000)
# Only if it refuses to exit (SIGKILL — no cleanup)
kill -9 $(lsof -ti :3000)
Prefer plain kill first. SIGTERM lets the process close database connections, flush logs and remove PID files; -9 skips all of that and can leave stale lock files that cause a different error on the next start.
If lsof is unavailable:
ss -lptn 'sport = :3000' # modern Linux
fuser -k 3000/tcp # find and kill in one step
Windows:
netstat -ano | findstr :3000
tasklist /FI "PID eq 41288"
taskkill /PID 41288 /F
Always identify before killing. A PID on port 3000 is usually another terminal tab running the same project — but not always, and kill -9 on the wrong one is an unpleasant surprise.
Fix 2: A Different Application Owns the Port
If lsof names something that is not your app, decide who should own the port rather than fighting over it. Common squatters:
| Port | Usual owner |
|---|---|
| 3000 | Another Node app, Grafana, Rails |
| 5000 | macOS AirPlay Receiver, Flask |
| 5432 | PostgreSQL |
| 8080 | Tomcat, Jenkins, a proxy |
| 27017 | MongoDB |
Port 5000 on macOS deserves a specific mention: AirPlay Receiver binds it by default, which makes a Flask or Express default-port app fail on a fresh Mac with no other server running. Turn it off in System Settings → General → AirDrop & Handoff → AirPlay Receiver, or move your app to another port.
Change your port when the other process has the better claim:
PORT=3001 npm start
const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`Listening on ${port}`));
Reading the port from the environment is worth doing regardless — it is what every hosting platform expects.
Fix 3: Your Own Code Binds Twice
If lsof shows exactly one process and it is the one you just started, you are binding twice in the same run. The usual causes:
app.listen()called in a module that gets imported more than once.- A test suite that starts the server in each file without closing it.
- A watcher that starts a new instance before the old one exits.
app.listen()inside a loop or a route handler.
The test case is the most common. Close the server when the suite finishes:
const server = app.listen(0); // port 0 = let the OS pick a free port
afterAll(() => new Promise(resolve => server.close(resolve)));
Passing 0 is the right move for tests generally — the OS assigns an unused port and parallel test files stop colliding. Read the real port back with server.address().port.
Fix 4: Docker
Inside a container the message can refer to the host-side publish rather than anything in the container:
docker ps --format "table {{.Names}}\t{{.Ports}}"
Two containers cannot publish the same host port. Stop the other one, or map to a different host port — the container port can stay the same:
docker run -p 3001:3000 myimage
Docker Compose fails the same way when two services declare the same host port, or when a previous docker compose up was interrupted and left containers running:
docker compose down
docker compose up
Fix 5: TIME_WAIT
If lsof -i :3000 lists no process but binding still fails, the socket is likely in TIME_WAIT — the kernel holds it briefly after close to catch stray packets. Confirm:
netstat -an | grep 3000
# tcp4 0 0 127.0.0.1.3000 127.0.0.1.54321 TIME_WAIT
It clears itself within a couple of minutes. Node already sets SO_REUSEADDR on servers, so this rarely blocks a listener in practice — if you hit it, waiting is the fix, and there is nothing to kill.
Handle It Instead of Crashing
An unhandled error event terminates the process. Attach a handler and fail with something readable:
const server = app.listen(port);
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`Port ${port} is already in use. Set PORT to something else, or run: lsof -ti :${port} | xargs kill`);
process.exit(1);
}
throw err;
});
Automatic fallback to another port is possible but usually a bad idea in development — the app appears to start, and then requests go to whichever copy is on the port you expected. Fail loudly instead.
Verify the Fix
Confirm the port is genuinely free before restarting:
lsof -i :3000 # no output means free
Then start the server and check it is listening:
npm start
curl -I http://localhost:3000
Prevention
- Read the port from
process.env.PORTwith a sensible default, so changing it never means editing code. - Stop servers with Ctrl-C, not by closing the terminal window. A closed window can orphan the child process, which is how a port ends up held by something with no visible owner.
- Use port
0in tests and close the server in teardown. - Add a script for the common case, so nobody has to remember the incantation:
"kill-port": "lsof -ti :3000 | xargs kill". - Check
docker psbefore blaming Node when the port seems held by nothing — a forgotten container is a frequent culprit.