DEV Community

Cover image for Error: listen EADDRINUSE: address already in use in Node.js: 10 Causes and Fixes (Tested on Linux, Windows tips, nodemon)
Mahir Faysal
Mahir Faysal

Posted on Originally published at mfaysal.com

Error: listen EADDRINUSE: address already in use in Node.js: 10 Causes and Fixes (Tested on Linux, Windows tips, nodemon)

"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})`);
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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.
  2. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

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...');
});
Enter fullscreen mode Exit fullscreen mode
$ kill -INT 284449
Got SIGINT, saving state...
$ ss -ltnH 'sport = :3000' | wc -l
1
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode
$ kill -INT 284505
SIGINT received, closing server...
server closed, port released
$ ss -ltnH 'sport = :3000' | wc -l
0
Enter fullscreen mode Exit fullscreen mode

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...');
});
Enter fullscreen mode Exit fullscreen mode
[nodemon] starting `node nm-trap.js`
listening on 3000 (pid 283568)
[nodemon] restarting due to changes...
SIGUSR2 received, cleaning up...
SIGUSR2 received, cleaning up...
Enter fullscreen mode Exit fullscreen mode

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'));
});
Enter fullscreen mode Exit fullscreen mode
[nodemon] restarting due to changes...
SIGUSR2 received, closing server...
[nodemon] starting `node nm-fixed.js`
listening on 3000 (pid 283702)
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
const app = require('./app');

app.listen(3000, () => console.log('index.js: listening on 3000'));
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}`));
Enter fullscreen mode Exit fullscreen mode
$ PORT=4567 node env-port.js
listening on 4567
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode
$ node port0.js
OS picked free port 42569
Enter fullscreen mode Exit fullscreen mode

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);
  }
});
Enter fullscreen mode Exit fullscreen mode
$ node retry.js
Port 3000 is busy, trying 3001...
Port 3001 is busy, trying 3002...
listening on http://localhost:3002
Enter fullscreen mode Exit fullscreen mode

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

  1. Stop dev servers with Ctrl+C, not Ctrl+Z, and don't close terminals with servers still running.
  2. Handle SIGINT and SIGTERM by closing the server and then exiting.
  3. Call listen() in one place only, and check the error in Express 5 callbacks.
  4. Read the port from PORT, and use port 0 in automated tests.
  5. 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.


Originally published on mfaysal.com.

I'm learning cybersecurity on TryHackMe — follow my progress at tryhackme.com/p/mfaysal.dev.

Top comments (0)