"Error: listen EADDRINUSE: address already in use :::3000" means another process is already listening on port 3000, so your Node.js server can't bind to it. The quick fix on Linux or macOS is to find that process with lsof -i :3000 and stop it with kill <PID>. On Windows, use netstat -ano | findstr :3000 and then taskkill /PID <PID> /F. If you need both servers running, start yours on a different port instead.
The Node.js docs on common system errors describe EADDRINUSE as a failed attempt to bind a server to a local address because another server already occupies it. The hard part is working out which server that is, because it's often a copy of your own app that you can't see.
I reproduced every cause below on Linux with Node.js 24.22.1 (plus 18, 20 and 22 for the basic error), Express 5.3.0 and 4.22.3, nodemon 3.1.14, Vite 8.3.6 and Next.js 16.4.5. All output is copied from those runs. The Windows commands are from Microsoft's docs and marked as untested.
In this guide:
- What the error looks like
- Quick fix: find and stop the process using the port
- Windows: netstat and taskkill (not tested)
- The 10 causes at a glance
- 1. The same app is already running in another terminal
- 2. A process suspended with Ctrl+Z still owns the port
- 3. An orphan left after nodemon or the terminal was killed
- 4. A Ctrl+C or SIGTERM handler that never exits
- 5. A nodemon restart that stalls on SIGUSR2
- 6. Express app.listen() called twice
- 7. A Next.js or Vite dev server already on that port
- 8. IPv4 vs IPv6: localhost is two addresses
- 9. A hard-coded port: use PORT or port 0
- 10. No error handler: retry on the next port
- How to stop it happening again
- Frequently asked questions
What the error looks like
Here's a minimal server, server.js:
const http = require('node:http');
const PORT = 3000;
const server = http.createServer((req, res) => {
res.end('Hello from ' + process.pid + '\n');
});
server.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT} (pid ${process.pid})`);
});
Running it a second time while the first copy was up crashed instantly:
$ node server.js
node:events:505
throw er; // Unhandled 'error' event
^
Error: listen EADDRINUSE: address already in use :::3000
at Server.setupListenHandle [as _listen2] (node:net:2390:16)
at listenInCluster (node:net:2499:12)
at Object.<anonymous> (/workspace/seo-posts-nov05/test/node/server.js:8:8)
Emitted 'error' event on Server instance at:
at emitErrorNT (node:net:2478:8)
at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {
code: 'EADDRINUSE',
errno: -98,
syscall: 'listen',
address: '::',
port: 3000
}
Node.js v24.22.1
A few details are worth reading closely:
:::3000 is the IPv6 "any address" :: followed by :3000. Node listens on :: by default when IPv6 is available.errno: -98 is the Linux number for this error. It differs on other systems, so check err.code in your code.- Unhandled 'error' event means nothing handled the server's
error event, so Node crashed with exit code 1.
Node 18, 20 and 22 printed the same first line and code.
Quick fix: find and stop the process using the port
On Linux and macOS, lsof shows who owns the port. The -t flag prints just the PID, which is handy for scripts:
$ lsof -i :3000
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
MainThrea 283150 box 21u IPv6 1804524 0t0 TCP *:3000 (LISTEN)
$ lsof -t -i :3000
283150
Node 24 shows up as MainThrea (MainThread, cut short), not node, so don't search the list for "node". Without lsof, ss and fuser give the same answer:
$ ss -ltnp 'sport = :3000'
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 511 *:3000 *:* users:(("MainThread",pid=283150,fd=21))
$ fuser 3000/tcp
3000/tcp: 283150
Check what it is before you kill it:
$ ps -o pid,ppid,etime,cmd -p 283150
PID PPID ELAPSED CMD
283150 283018 00:01 /usr/local/lib/nodejs/node server.js
$ kill 283150
$ lsof -i :3000
lsof exit code: 1
A plain kill sends SIGTERM, so the process can clean up. fuser -k 3000/tcp finds and kills in one step, but sends SIGKILL. For more ways to list listening ports, see my guide on how to check open ports.
Windows: netstat and taskkill (not tested)
I didn't run these, because my test machine is Linux. They come from Microsoft's documentation for netstat and taskkill:
- In Command Prompt or PowerShell, run
netstat -ano | findstr :3000. The -o flag adds the owning process ID in the last column of the LISTENING line. - Stop it with
taskkill /PID 12345 /F, replacing 12345 with that PID. /F forces termination.
The 10 causes at a glance
| # | Cause | How to spot it | Fix |
| 1 | The same app is running in another terminal | lsof -i :3000 shows your own script | Stop it, or use another port |
| 2 | A process suspended with Ctrl+Z | ps shows state T; requests hang | fg then Ctrl+C, or kill |
| 3 | An orphan left after nodemon or a terminal was killed | Parent PID changed to an init process | Kill the orphan PID |
| 4 | A SIGINT/SIGTERM handler that never exits | Ctrl+C prints a message but the port stays busy | Close the server, then exit |
| 5 | A nodemon SIGUSR2 handler that never exits | Restart prints "restarting" and stalls | Use process.once and re-send the signal |
| 6 | Express app.listen() called twice | Two "listening" logs from one process | Listen in one file only |
| 7 | A Next.js or Vite dev server on the same port | Fixed port flag or strictPort | Stop the other server or change port |
| 8 | IPv4 and IPv6 binding overlap | Error shows :::3000 or 127.0.0.1:3000 | Pick one host deliberately |
| 9 | A hard-coded port | Every project uses 3000 | PORT env var, or port 0 in tests |
| 10 | No error handler on the server | "Unhandled 'error' event" crash | server.on('error') with a retry |
1. The same app is already running in another terminal
This is the usual cause. You started the server in another terminal tab, a VS Code task or a background job, forgot about it, and started it again. The crash in the first section came from exactly this.
Fix: find the PID, confirm it's your app with ps, and stop it. If you want two copies, give the second its own port (see cause 9).
2. A process suspended with Ctrl+Z still owns the port
Ctrl+Z doesn't stop a server. It pauses it, and the paused process still owns the port. I paused one with SIGSTOP, which is what Ctrl+Z sends:
$ ps -o pid,stat,cmd -p 283234
PID STAT CMD
283234 Tl /usr/local/lib/nodejs/node server.js
$ curl -m 3 http://localhost:3000
curl: (28) Operation timed out after 3001 milliseconds with 0 bytes received
$ node server.js
Error: listen EADDRINUSE: address already in use :::3000
T means stopped. A hanging request plus a busy port is the giveaway.
Fix: in the same terminal, run jobs to list paused jobs, then fg to bring it back and Ctrl+C to quit properly. From another terminal, kill -CONT <PID> resumes it and kill <PID> stops it. After I sent SIGCONT, the paused server answered again with Hello from <pid>.
3. An orphan left after nodemon or the terminal was killed
nodemon runs your app as a child process. If nodemon dies hard (kill -9, a crashed editor terminal, a dropped SSH session), the child can survive. I killed nodemon with SIGKILL and the Node process got a new parent:
$ ps -o pid,ppid,cmd -p 283361 (before)
PID PPID CMD
283361 283346 /usr/local/lib/nodejs/node server.js
$ ps -o pid,ppid,cmd -p 283361 (after nodemon was killed)
PID PPID CMD
283361 53 /usr/local/lib/nodejs/node server.js
Starting nodemon again then failed:
$ npx nodemon server.js
[nodemon] 3.1.14
[nodemon] starting `node server.js`
Error: listen EADDRINUSE: address already in use :::3000
[nodemon] app crashed - waiting for file changes before starting...
Fix: kill the orphan by PID (lsof -t -i :3000 finds it), then start nodemon again. Quit nodemon with Ctrl+C rather than killing it, so it can stop its child first.
4. A Ctrl+C or SIGTERM handler that never exits
Once you add your own SIGINT handler, Node stops exiting on Ctrl+C by itself. If the handler only logs or saves something, the process keeps running and keeps the port:
// Mistake: catching Ctrl+C but never closing the server or exiting
process.on('SIGINT', () => {
console.log('Got SIGINT, saving state...');
});
$ kill -INT 284449
Got SIGINT, saving state...
$ ss -ltnH 'sport = :3000' | wc -l
1
The listener survived the "shutdown". Instead, close the server, exit when it's closed, and force an exit if connections hold it open. closeIdleConnections() and closeAllConnections() are documented in the Node.js http module reference:
function shutdown(signal) {
console.log(`${signal} received, closing server...`);
server.close(() => {
console.log('server closed, port released');
process.exit(0);
});
server.closeIdleConnections();
// Safety net: force exit if open connections keep the server alive
setTimeout(() => {
console.log('forcing exit after 5s');
server.closeAllConnections();
process.exit(1);
}, 5000).unref();
}
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
$ kill -INT 284505
SIGINT received, closing server...
server closed, port released
$ ss -ltnH 'sport = :3000' | wc -l
0
Handle SIGTERM too, because Docker, systemd and most process managers use it instead of Ctrl+C.
5. A nodemon restart that stalls on SIGUSR2
On each restart, nodemon sends your app a signal (SIGUSR2 by default) and waits for it to exit. The nodemon README explains how to catch that signal for cleanup. If your handler catches it and doesn't exit, the restart stalls:
// Mistake: a "cleanup" handler that never lets the process exit
process.on('SIGUSR2', () => {
console.log('SIGUSR2 received, cleaning up...');
});
[nodemon] starting `node nm-trap.js`
listening on 3000 (pid 283568)
[nodemon] restarting due to changes...
SIGUSR2 received, cleaning up...
SIGUSR2 received, cleaning up...
nodemon 3.1.14 waited for the old process, so I got a frozen restart rather than EADDRINUSE. Kill nodemon at that point and you get the orphan from cause 3. This version cleans up, then lets the signal finish the job:
// nodemon sends SIGUSR2 on restart: clean up, then let the signal kill us
process.once('SIGUSR2', () => {
console.log('SIGUSR2 received, closing server...');
server.close(() => process.kill(process.pid, 'SIGUSR2'));
});
[nodemon] restarting due to changes...
SIGUSR2 received, closing server...
[nodemon] starting `node nm-fixed.js`
listening on 3000 (pid 283702)
once matters here. It removes the handler after the first call, so the re-sent signal uses the default action and ends the process.
6. Express app.listen() called twice
A common Express layout has app.js build the app and index.js start it. The bug creeps in when app.js also calls listen():
// Mistake: the app module starts listening as a side effect
app.listen(3000, () => console.log('app.js: listening on 3000'));
module.exports = app;
const app = require('./app');
app.listen(3000, () => console.log('index.js: listening on 3000'));
Express 4.22.3 crashed with the usual EADDRINUSE trace. Express 5.3.0 behaved differently, and the result is confusing:
$ node express-bad/index.js (Express 5)
app.js: listening on 3000
index.js: listening on 3000
$ ss -ltnH 'sport = :3000' | wc -l
1
Both log lines appeared, but only one server was listening. In Express 5, app.listen() passes a listen error to your callback, so a callback that ignores its argument logs "listening" even when it failed. Logging the argument shows the truth:
app.js: listening on 3000
index.js callback, err = listen EADDRINUSE: address already in use :::3000
Fix: export the app without listening, call listen() in one entry file, and always check err in Express 5. The Express 5 API reference covers app.listen().
const PORT = Number(process.env.PORT) || 3000;
const server = app.listen(PORT, (err) => {
if (err) {
console.error(`Could not start: ${err.code} on port ${PORT}`);
process.exit(1);
}
console.log(`listening on ${PORT}`);
});
7. A Next.js or Vite dev server already on that port
Front-end dev servers handle a busy port in their own way. With port 3000 taken, Next.js 16.4.5 moved to the next free port when I didn't name one, but failed when I asked for 3000 explicitly:
$ npx next dev
⚠ Port 3000 is in use by process 284021, using available port 3001 instead.
- Local: http://localhost:3001
$ npx next dev -p 3000
⨯ Failed to start server
Error: listen EADDRINUSE: address already in use :::3000
Vite 8.3.6 does the same. It tries the next port unless you set --strictPort or server.strictPort, and then it exits with its own message. Without the flag, it came up on 5174:
$ npx vite --host 127.0.0.1
Port 5173 is in use, trying another one...
$ npx vite --host 127.0.0.1 --strictPort
error when starting dev server:
Error: Port 5173 is already in use
The silent fallback can break a CORS allow-list, OAuth redirect URL or proxy that expects the original port. If a fixed port matters, use strict mode and stop the old process.
8. IPv4 vs IPv6: localhost is two addresses
The net module docs say that without a host, Node listens on ::, and on most systems that also covers IPv4 0.0.0.0. I used a small listen-host.js that takes a port and an optional host. With something on 127.0.0.1:3000, the default listen failed:
listening on 127.0.0.1 port 3000 (family IPv4)
$ node listen-host.js 3000
default -> EADDRINUSE: listen EADDRINUSE: address already in use :::3000
A server bound to IPv6 ::1 could share port 3000 with the IPv4 one, though, and localhost then went to the IPv6 server:
$ ss -ltn 'sport = :3000'
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 511 127.0.0.1:3000 0.0.0.0:*
LISTEN 0 511 [::1]:3000 [::]:*
$ curl http://localhost:3000
answered by ::1
Node resolved localhost to ::1 first here. The address in the error tells you which binding clashed, so search by port with ss -ltn and choose your host on purpose.
9. A hard-coded port: use PORT or port 0
If every project uses 3000, two will collide eventually. Read the port from the environment:
const PORT = Number(process.env.PORT) || 3000;
const server = http.createServer((req, res) => res.end('ok\n'));
server.listen(PORT, () => console.log(`listening on ${server.address().port}`));
$ PORT=4567 node env-port.js
listening on 4567
For tests, listen on port 0. The operating system then picks a free port, so parallel test runs can't clash:
server.listen(0, '127.0.0.1', () => {
const { port } = server.address();
console.log(`OS picked free port ${port}`);
server.close();
});
$ node port0.js
OS picked free port 42569
Read server.address() inside the listen callback. Before the server is listening, it returns null, and reading .port from it throws the kind of error covered in my post on Cannot read properties of undefined.
10. No error handler: retry on the next port
The crash says "Unhandled 'error' event" because nobody listened for it. Add a handler, and you can try the next port, much as Vite does:
server.on('error', (err) => {
if (err.code === 'EADDRINUSE' && port < lastPort) {
console.log(`Port ${port} is busy, trying ${port + 1}...`);
port += 1;
setTimeout(() => server.listen(port), 100);
} else {
console.error(err);
process.exit(1);
}
});
$ node retry.js
Port 3000 is busy, trying 3001...
Port 3001 is busy, trying 3002...
listening on http://localhost:3002
Use this for local tools only. In production, a server that quietly changes port is worse than one that fails loudly, because your proxy still points at the old one.
Node 22.12+ also accepts reusePort: true on Linux. Two processes that both set it shared port 3000 in my test, but a normal server still got EADDRINUSE. It's for load balancing, not for hiding stale processes.
How to stop it happening again
- Stop dev servers with Ctrl+C, not Ctrl+Z, and don't close terminals with servers still running.
- Handle SIGINT and SIGTERM by closing the server and then exiting.
- Call
listen() in one place only, and check the error in Express 5 callbacks. - Read the port from
PORT, and use port 0 in automated tests. - Add a
server.on('error') handler that prints a clear message.
For other quick developer helpers, see my free tools.
Frequently asked questions
What does EADDRINUSE mean in Node.js?
It means address already in use. Another process, often an earlier copy of your own app, is already listening on the port and address your server tried to bind, so the operating system refused the second listen() call.
How do I kill the process on port 3000?
On Linux or macOS, run lsof -i :3000 or ss -ltnp 'sport = :3000' to get the PID, check it with ps, then run kill PID. fuser -k 3000/tcp does both steps but sends SIGKILL. On Windows, netstat -ano | findstr :3000 shows the PID and taskkill /PID PID /F stops it (I didn't test the Windows commands).
Why does EADDRINUSE keep happening with nodemon?
Usually because an old copy of the app survived. That happens if nodemon was killed hard, if the app was paused with Ctrl+Z, or if another nodemon is running in a second terminal. A SIGUSR2 handler that never exits makes nodemon's restart stall instead.
Why does the error say :::3000?
The :: is the IPv6 any address, and :3000 is the port. Node listens on :: by default when IPv6 is available, which usually covers IPv4 too, so it clashes with anything on 0.0.0.0:3000 or 127.0.0.1:3000.
Is EACCES the same as EADDRINUSE?
No. EACCES means permission denied. On Linux, ports below 1024 need extra privileges, so listen(80) as a normal user gave me listen EACCES: permission denied 0.0.0.0:80. Use a port above 1024 and put a reverse proxy in front.
Can two Node.js processes listen on the same port?
Not normally. On Linux with Node 22.12+ or 23.1+, processes that all set reusePort: true can share a port, and the cluster module shares one between workers. A normal listen() on a port that's already in use still fails with EADDRINUSE.
How do I make my server pick a free port automatically?
Listen on port 0 and read server.address().port in the listen callback, or catch EADDRINUSE in server.on('error') and retry on the next port. Both are fine for local development and tests. In production, use a fixed, configured port.
Last checked: 5 November 2026. Figures, settings and guidance can change, so confirm anything important with the sources linked above.