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
| Cause | What we did | Result |
|---|---|---|
| Old server still running | Second node srv.js 8983 | EADDRINUSE :::8983, exit 1 |
Two copies started together (watchers, two terminals, npm run dev twice) | Launched two processes at the same moment | First one listened; second crashed with EADDRINUSE |
| Server suspended with Ctrl+Z | kill -STOP on the server | Process state Tl, still LISTEN in ss, new server got EADDRINUSE. Run fg then Ctrl+C, or kill the PID |
| Another program on the port | python3 -m http.server 8983 | Node got EADDRINUSE; ss showed python3 |
listen() called twice in one process | s.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 bound | Node host | Result |
|---|---|---|
| 0.0.0.0 (Python default) | none, ::, 0.0.0.0, 127.0.0.1 | EADDRINUSE |
::1, 'localhost' | OK, listening on ::1 | |
| 127.0.0.1 | none, ::, 0.0.0.0, 127.0.0.1 | EADDRINUSE |
::1, 'localhost' | OK, listening on ::1 | |
:: (dual-stack) | none, 0.0.0.0, 127.0.0.1 | EADDRINUSE |
| 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:
- Node
listen(8983)right after: OK, both times. - Raw Python socket with no
SO_REUSEADDR:[Errno 98] Address already in use. - Raw Python socket with
SO_REUSEADDR: OK.
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
- Run
ss -ltnp 'sport = :PORT'(orlsof -i :PORT). - Check the PID with
ps -o pid,args -p PID. On Node 24 the name may say MainThread. - If it is yours,
kill PID. If it is suspended (state T),fgand Ctrl+C, or kill it. - If it is not yours, pick another port with
PORT=3001. - 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.