Skip to main content
DevOpsbeginner

Fix "EADDRINUSE: address already in use" in Node

Fix `Error: listen EADDRINUSE: address already in use :::3000`. Find and stop the process holding the port instead of just changing ports.

8 min readUpdated August 2026

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.

CauseTell
1. A previous run of your own app is still alivelsof shows node, often more than one
2. A different application owns the portlsof shows something else entirely
3. Your code binds the same port twiceOnly one process in lsof, and it is yours
4. A container publishes the portlsof shows com.docke or nothing useful
5. The socket is in TIME_WAITNo 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:

PortUsual owner
3000Another Node app, Grafana, Rails
5000macOS AirPlay Receiver, Flask
5432PostgreSQL
8080Tomcat, Jenkins, a proxy
27017MongoDB

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.

Advertisement

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.PORT with 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 0 in 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 ps before blaming Node when the port seems held by nothing — a forgotten container is a frequent culprit.

Frequently Asked Questions

Find answers to common questions

Another process is already listening on the port and address your server asked for, and the operating system will not hand the same combination to two processes. It is an OS-level refusal, so nothing in your Node code can bind that port until the other process releases it.

On macOS or Linux run 'lsof -i :3000' to see the PID and command holding it. On Windows run 'netstat -ano | findstr :3000' and match the PID with 'tasklist'. Identify the process before killing it — it is often another terminal running the same project.

Get the PID with 'lsof -ti :3000' and stop it with 'kill $(lsof -ti :3000)'. Use plain kill first, which sends SIGTERM and lets the process shut down cleanly; only add -9 if it refuses to exit, since SIGKILL skips cleanup.

Either the process did not actually exit — common with nodemon, a detached process, or a crashed parent leaving an orphaned child — or the socket is in TIME_WAIT. TIME_WAIT clears itself within a couple of minutes, and setting SO_REUSEADDR, which Node does by default for servers, avoids it.

It is the IPv6 wildcard address, meaning the server is listening on all interfaces. When you bind without specifying a host, Node listens on both IPv6 and IPv4 wildcards, so a conflict on either family produces this error.

It works, but usually hides the problem. If a stale copy of your own app is holding the port, changing ports leaves it running and consuming resources, and the conflict reappears on the next restart. Find out what is on the port before you move.

Attach an error handler to the server: server.on('error', err => { if (err.code === 'EADDRINUSE') ... }). Without one, the error event goes unhandled and Node exits. With one you can log a clear message, retry, or fall back to another port deliberately.

Two containers cannot publish the same host port, and the message may refer to the host side rather than anything inside the container. Run 'docker ps' and check the PORTS column for an existing publish of that port, then stop that container or map to a different host port.

Not on the same address. You can bind the same port on different addresses — for example 127.0.0.1:3000 and 192.168.1.5:3000 — because the address and port together form the binding. Node's cluster module also shares one port across workers, but that is one listening socket shared by the parent.