Quick Answer

EADDRINUSE means the operating system refused to bind your server to a port because another process is already listening on it. Almost always that process is an earlier run of your own app that never exited. Find it with netstat and taskkill on Windows, or lsof and kill on macOS and Linux, then restart. The long-term fix is reading the port from process.env.PORT and shutting the server down cleanly on SIGINT and SIGTERM.

What EADDRINUSE actually tells you

A TCP port can have exactly one listening socket at a time. When your server calls listen(3000), it asks the operating system to reserve that port. If something else already holds it, the kernel refuses and Node surfaces the refusal as an error event on the server object. Because nothing is listening for that event by default, the process dies.

Error: listen EADDRINUSE: address already in use :::3000

Read the last part carefully. :::3000 is the IPv6 wildcard address, which is what you get when you call app.listen(3000) without a host. 0.0.0.0:3000 is the IPv4 wildcard, and 127.0.0.1:3000 means loopback only. Those are three different bind targets and the message tells you which one failed.

The important thing to internalise is that this is not a bug in your code. Your file parsed, your imports resolved, your middleware registered. The failure happened at the very last step, when the process asked the kernel for a resource that was already handed out. That is why editing your routes, reinstalling node_modules or deleting package-lock.json never helps, and why people waste an hour doing exactly that.

There are only two possibilities. Either another program is genuinely listening on that port, or the port is reserved by the system so that no process can take it. The first is common, the second happens mostly on Windows. Everything below is about telling those two apart quickly instead of guessing.

Finding and killing the process that holds the port

Do not reach for a random kill -9 command from a forum post. Find the process first, look at what it is, then decide. On Windows, ask for the owning process ID and then stop it:

netstat -ano | findstr :3000
taskkill /PID 18244 /F

The last column of the netstat output is the PID. In PowerShell you can do the same thing with objects instead of text parsing, which is less error prone because findstr :3000 also matches unrelated ports such as 30001:

Get-NetTCPConnection -LocalPort 3000 -State Listen |
  Select-Object OwningProcess,
    @{n='Name';e={(Get-Process -Id $_.OwningProcess).ProcessName}}

Stop-Process -Id 18244 -Force

On macOS and Linux, lsof gives you the command name in the same output, which is exactly what you want before killing anything:

lsof -i :3000
kill 12345          # polite, sends SIGTERM
kill -9 12345       # only if it ignores SIGTERM

On most Linux distributions ss is installed and lsof may not be. ss -ltnp | grep :3000 does the same job, and fuser -k 3000/tcp kills the holder in one step. If you would rather not remember any of this, npx kill-port 3000 works on all three platforms.

Two warnings. Send SIGTERM before SIGKILL, because kill -9 gives your app no chance to flush logs or close database connections. And if the owning process turns out to be System with PID 4 on Windows, or something you did not start, stop and read the next sections rather than force killing a system service.

Why nodemon and your editor keep leaving servers alive

The reason this error feels like it follows you around is that a Node server is very easy to orphan. Nodemon works by spawning your app as a child process, killing it when a file changes, and spawning it again. If your app spawns children of its own, or you run it through a shell wrapper such as npm run dev inside nodemon --exec, the signal reaches the wrapper and not the actual server. The wrapper exits, nodemon reports a clean restart, and the real listener carries on holding the port.

Closing a VS Code terminal tab is another reliable way to orphan a process, because the tab disappears before the shell forwards a signal. On macOS and Linux, pressing Ctrl+Z instead of Ctrl+C only suspends the job; the process still owns the socket. Bring it back with fg and stop it properly, or kill %1.

A subtler cause is a SIGINT handler you wrote yourself. The moment you register one, you take responsibility for exiting:

process.on('SIGINT', () => {
  console.log('Shutting down...');
  // no process.exit() and no server.close():
  // Ctrl+C now does nothing at all
});

Also check for a second listener you forgot about. Importing your app.js from a test file that itself calls listen, or leaving pm2 running from a deployment experiment, both produce a port holder with no visible terminal. Run pm2 list and docker ps before assuming the machine is clean. A container published with -p 3000:3000 holds the host port just as firmly as a local process does.

Changing the port properly

Hardcoding 3000 is fine on your laptop and wrong everywhere else, because most hosting platforms hand your process a port through an environment variable and expect you to use it. The standard shape is one line:

const PORT = process.env.PORT || 3000;
const server = app.listen(PORT, () => {
  console.log(`Listening on http://localhost:${PORT}`);
});

Now you can start a second copy with PORT=4000 npm run dev on macOS and Linux, or $env:PORT=4000; npm run dev in PowerShell, without editing any code. Next, turn the crash into a message a human can act on by handling the server's error event:

server.on('error', (err) => {
  if (err.code === 'EADDRINUSE') {
    console.error(`Port ${PORT} is already in use. Set PORT to a free port.`);
    process.exit(1);
  }
  throw err;
});

Then close cleanly so the port is released the moment you stop the app:

for (const signal of ['SIGINT', 'SIGTERM']) {
  process.on(signal, () => {
    server.close(() => process.exit(0));
  });
}

One catch worth knowing: server.close() stops accepting new connections but waits for existing keep-alive sockets, so a browser tab with an open connection can delay shutdown for seconds. Newer Node versions provide server.closeAllConnections(), which destroys those sockets immediately; call it yourself after a short grace period if server.close() has not finished by then.

For tests, do not pick a port at all. app.listen(0) asks the kernel for any free port, and server.address().port tells you which one you got. That makes parallel test runs impossible to collide, which matters once your CI runs several suites at once.

When it is not your app at all

Some ports are contested by software you did not install for development. On recent macOS versions the AirPlay Receiver service listens on port 5000, which is why a Flask or Express tutorial that picks 5000 fails on a Mac and works everywhere else. Turn AirPlay Receiver off in System Settings or move to another port. On Windows, XAMPP and IIS fight over 80, MySQL takes 3306, and older versions of some chat apps grabbed 80 and 443 at boot.

Windows has one failure mode that confuses everyone, because netstat shows nothing listening yet the bind still fails. Hyper-V and WSL2 reserve blocks of ports for their own use, and any port inside a reserved range is unavailable to you. Check the ranges:

netsh interface ipv4 show excludedportrange protocol=tcp

If your port sits inside one of those ranges, either pick a port outside them or restart the NAT service from an administrator prompt with net stop winnat followed by net start winnat, which usually reshuffles the reservations.

On macOS and Linux, ports below 1024 are privileged. Trying to listen on 80 as a normal user gives you EACCES, not EADDRINUSE, and no amount of process hunting will help. Use a high port in development and put nginx or a load balancer in front in production.

Finally, ignore advice about sockets stuck in TIME_WAIT. That state applies to connections, not to listening sockets, and Node sets SO_REUSEADDR on Linux and macOS so a restarted server can rebind immediately. If a port frees up after you wait a few seconds, the far more likely explanation is that the previous process was still finishing its shutdown.

Frequently Asked Questions

Why does the error say :::3000 instead of localhost:3000? Three colons is the IPv6 wildcard address, written as :: followed by the port. Calling app.listen(3000) with no host binds to the wildcard, and on a dual-stack system that covers IPv4 traffic too. If you see 0.0.0.0:3000 instead, the bind was IPv4 only, and 127.0.0.1:3000 means the server was restricted to loopback so nothing on your network could reach it.
Is kill -9 safe to use on my dev server? It works, but it is a blunt instrument. SIGKILL cannot be caught, so your app gets no chance to close database connections, finish writing logs or remove a lock file. Try a plain kill first, which sends SIGTERM and lets your shutdown handler run. Reserve kill -9 for a process that is genuinely stuck and ignoring SIGTERM.
Why does my server say the port is free but the browser still shows the old page? That is caching, not EADDRINUSE. The browser is serving an old response from its own cache or from a service worker registered earlier. Hard reload with Ctrl+Shift+R, and check the Application tab in DevTools for a service worker that is intercepting requests. The port and the cache are unrelated problems that often appear together after a restart.
Can two servers share one port if they use different paths? No. Paths are an HTTP concept and the conflict happens at the TCP layer, well before any HTTP request is parsed. To serve /api and /app on the same port, run one process that handles both, or put a reverse proxy such as nginx on the port and have it forward to two backend servers on different internal ports.
Should I write code that automatically tries the next free port? In development it is convenient, and tools like Vite do exactly that. In production it is a bad idea, because your load balancer, firewall rules and health checks all expect a known port. If the configured port is unavailable in production, that is a deployment problem and the process should log a clear message and exit rather than silently move somewhere nobody is looking.