What you'll learn
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 :::3000Read 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 /FThe 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 -ForceOn 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 SIGTERMOn 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=tcpIf 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.
