EADDRINUSE: address already in use (tested)

Error: listen EADDRINUSE: address already in use :::3000 means another socket is already listening on that port. Usually it is an old copy of your own server. We reproduced it on Node 24 every way we could think of, found the owner with ss, lsof and fuser, and tested the fixes.

1. The exact error

We started a Node server on port 8983, then ran require('http').createServer().listen(8983) in a second process with no error handler. Node 24.21.0 crashed with exit code 1:

node:events:505
    throw er; // Unhandled 'error' event
    ^

Error: listen EADDRINUSE: address already in use :::8983
    at Server.setupListenHandle [as _listen2] (node:net:2328:16)
    at listenInCluster (node:net:2437:12)
    at Server.listen (node:net:2562:7)
    ...
Emitted 'error' event on Server instance at:
    at emitErrorNT (node:net:2416:8)
    at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {
  code: 'EADDRINUSE',
  errno: -98,
  syscall: 'listen',
  address: '::',
  port: 8983
}

The :::8983 is not a typo. It is the address :: (all IPv6 and IPv4 addresses) followed by :8983. When you call listen(port) with no host, Node binds to ::. If you pass '127.0.0.1' the message becomes address already in use 127.0.0.1:8983.

Note that it is an event, not a thrown exception. A try/catch around listen() does not catch it. You need server.on('error', ...) (section 7).

2. Find what holds the port

All three tools pointed at our own test server (PID 1559926). On Linux, ss is always there; lsof and fuser may need installing.

$ ss -ltnp 'sport = :8983'
State  Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0      511                *:8983            *:*    users:(("MainThread",pid=1559926,fd=21))

$ lsof -i :8983
COMMAND       PID   USER FD   TYPE   DEVICE SIZE/OFF NODE NAME
MainThrea 1559926 jacque 21u  IPv6 21087262      0t0  TCP *:8983 (LISTEN)

$ lsof -t -i :8983 -sTCP:LISTEN
1559926

$ fuser -v -n tcp 8983
                     USER        PID ACCESS COMMAND
8983/tcp:            jacque    1559926 F.... MainThread

Surprise: on Node 24 the process name shows as MainThread, not node. Do not grep for "node" in this output. Check the PID instead:

$ ps -o pid,args -p 1559926
    PID COMMAND
1559926 node srv.js 8983

Without -p, ss only shows processes you own. A port held by another user or by root shows no process column unless you run it as root.

3. Kill it by PID, not by name

kill 1559926        # SIGTERM; our server exited with code 143, port free at once
kill -9 1559926     # only if it ignores SIGTERM

Avoid killall node or pkill node. They also kill your editor's language server, other projects and anything else running on Node. kill $(lsof -t -i :3000 -sTCP:LISTEN) is fine for a dev box because it only targets the listener.

4. Common causes we reproduced

CauseWhat we didResult
Old server still runningSecond node srv.js 8983EADDRINUSE :::8983, exit 1
Two copies started together (watchers, two terminals, npm run dev twice)Launched two processes at the same momentFirst one listened; second crashed with EADDRINUSE
Server suspended with Ctrl+Zkill -STOP on the serverProcess state Tl, still LISTEN in ss, new server got EADDRINUSE. Run fg then Ctrl+C, or kill the PID
Another program on the portpython3 -m http.server 8983Node got EADDRINUSE; ss showed python3
listen() called twice in one processs.listen(8983); s.listen(8983)Different error: ERR_SERVER_ALREADY_LISTEN "Listen method has been called more than once without closing." It is thrown, so try/catch works here

Ctrl+C on a server with no signal handler is not a cause. Our server exited with code 130 and a new one could listen right away.

5. IPv4 vs IPv6: when two servers share a port

We bound Python first, then tried Node with each host. This box has the Linux default bindv6only=0, so :: also covers IPv4.

Already boundNode hostResult
0.0.0.0 (Python default)none, ::, 0.0.0.0, 127.0.0.1EADDRINUSE
::1, 'localhost'OK, listening on ::1
127.0.0.1none, ::, 0.0.0.0, 127.0.0.1EADDRINUSE
::1, 'localhost'OK, listening on ::1
:: (dual-stack)none, 0.0.0.0, 127.0.0.1EADDRINUSE
127.0.0.1{host:'::', ipv6Only:true}OK

The trap is listen(port, 'localhost'). Node 24 resolved localhost to ::1, so it did not conflict with Python on 127.0.0.1. Both ran on the same port. Then curl http://localhost:8983/ and Node fetch reached the Node server, while curl http://127.0.0.1:8983/ reached Python. No error, just the wrong app answering. If a request hits "the old version" of your app, run ss -ltnp 'sport = :PORT' and look for two lines.

6. TIME_WAIT is not the problem in Node

We held a client connection open, killed the server (SIGKILL and SIGTERM), and restarted at once. ss -tan showed the old connections in TIME-WAIT on port 8983. Results:

So Node behaves like a socket with SO_REUSEADDR set (libuv sets it on Unix, per the libuv source, not traced here). If Node reports EADDRINUSE, a process really is listening. Look for it; do not wait for TIME_WAIT to expire.

7. Fixes in code

Handle the 'error' event so you get a clear message instead of a stack trace:

const server = app.listen(PORT);
server.on('error', (err) => {
  if (err.code === 'EADDRINUSE') {
    console.error(`Port ${PORT} is in use. Find it: ss -ltnp 'sport = :${PORT}'`);
    process.exit(1);
  }
  throw err;
});

We measured the fields on err: code 'EADDRINUSE', errno -98, syscall 'listen', address '::', port 8983. Don't silently switch to another port in production; your proxy will still point at the old one.

Close the server on SIGTERM and SIGINT so watchers and process managers can restart cleanly:

for (const sig of ['SIGTERM', 'SIGINT']) {
  process.on(sig, () => {
    server.close(() => process.exit(0));
    setTimeout(() => server.closeAllConnections(), 5000).unref();
  });
}

Measured: with only an idle keep-alive client, server.close() finished in 2 ms (Node 24 drops idle connections). With a 2 s request in flight, it waited 1718 ms, the request completed (slow done), then the process exited 0. The timeout is a safety net for clients that never finish.

Use port 0 in tests. The OS picks a free port, so parallel test runs never collide:

server.listen(0, '127.0.0.1', () => {
  const { port } = server.address();   // we got 36081
  // fetch(`http://127.0.0.1:${port}/`) ...
});

8. Quick checklist

  1. Run ss -ltnp 'sport = :PORT' (or lsof -i :PORT).
  2. Check the PID with ps -o pid,args -p PID. On Node 24 the name may say MainThread.
  3. If it is yours, kill PID. If it is suspended (state T), fg and Ctrl+C, or kill it.
  4. If it is not yours, pick another port with PORT=3001.
  5. Add the error handler and signal handlers above.

The opposite problem, nothing listening at all, gives ERR_CONNECTION_REFUSED / ECONNREFUSED. If a server dies mid-request you see ECONNRESET or socket hang up.

Frequently asked questions

Why does the error say :::3000?

It is the IPv6 'any' address :: followed by :3000. Node binds to :: when you call listen(port) with no host. On Linux with default settings that also covers IPv4, so a server on 0.0.0.0 or 127.0.0.1 blocks it.

How do I find which process is using the port?

Run ss -ltnp 'sport = :3000', or lsof -i :3000, or fuser -v -n tcp 3000. In our test all three showed the same PID. On Node 24 the process name showed as MainThread, so confirm with ps -o pid,args -p PID.

Do I need to wait for TIME_WAIT before restarting?

Not with Node. We killed a server with an open connection and restarted at once while ss showed TIME-WAIT entries; Node listened fine. A raw socket without SO_REUSEADDR failed with Errno 98 in the same state.

Why can't try/catch catch EADDRINUSE?

listen() is asynchronous. The failure arrives as an 'error' event on the server. Attach server.on('error', ...) and check err.code === 'EADDRINUSE'. Without a listener Node crashes with 'Unhandled error event'.

Two apps run on the same port without error. How?

One listens on 127.0.0.1 and the other on ::1. Node 24 resolved 'localhost' to ::1 in our test, so listen(port, 'localhost') did not clash with a 127.0.0.1 server. curl localhost reached one app and curl 127.0.0.1 the other.

Written with AI assistance. Measured on 2026-10-02 with Node.js 24.21.0, Python 3.13.5, curl, ss (iproute2), lsof and fuser on Debian 13 (Linux 6.12, net.ipv6.bindv6only=0). We only inspected and killed our own test processes, by PID. PlainHeaders is our extension; Node.js, ss, lsof and fuser are free.