Fix: listen EADDRINUSE: address already in use (Node.js)
Part of: JavaScript & TypeScript Errors
Quick Answer
Fix Node.js 'listen EADDRINUSE: address already in use': find and kill the process on the port (macOS, Linux, Windows), or run your server on a different port.
The first time I hit this, I had three terminal tabs open and no idea which one still had a server running. In my experience EADDRINUSE is almost never mysterious once you accept the one thing it always means: some process is already listening on the port you asked for, and Node refuses to fight it for the socket. I once wasted twenty minutes restarting my editor before realizing a node process from a closed tab was still alive in the background. The fix takes about ten seconds when you know which process to look for, so I keep a one-line command handy because I hit this so often. What follows is every angle I have needed over the years, from the ten-second kill to the reasons it keeps coming back in tests and Docker.
Just need the port back? This kills whatever is holding it, on any OS:
npx kill-port 3000Read on for how to find the culprit yourself, why it happened, and how to stop it recurring.
listen EADDRINUSE: address already in use :::3000
Your Node server crashes on startup with an unhandled error event:
node:events:496
throw er; // Unhandled 'error' event
^
Error: listen EADDRINUSE: address already in use :::3000
at Server.setupListenHandle [as _listen2] (node:net:1817:16)
at listenInCluster (node:net:1865:12)
at Server.listen (node:net:1953:7)
at Object.<anonymous> (/app/server.js:12:8)
at Module._compile (node:internal/modules/cjs/loader:1254:14)
Emitted 'error' event on Server instance at:
at emitErrorNT (node:net:1844:8)
at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
code: 'EADDRINUSE',
errno: -98,
syscall: 'listen',
address: '::',
port: 3000
}The :::3000 is not a typo. It is the IPv6 wildcard address :: followed by :3000, which means Node tried to bind every interface on port 3000. If your server binds IPv4, the same error reads address already in use 0.0.0.0:3000. The errno also varies by platform: -98 on Linux, -48 on macOS, and a positive EADDRINUSE on Windows.
The same failure surfaces with different wording depending on where it happens:
Error: listen EADDRINUSE: address already in use 127.0.0.1:8080# Docker, when the host port mapping clashes
Error response from daemon: driver failed programming external connectivity:
Bind for 0.0.0.0:3000 failed: port is already allocatedThe syscall in the message reads bind instead of listen when the collision is on a UDP (dgram) socket or a cluster worker rather than an HTTP server:
Error: bind EADDRINUSE 0.0.0.0:41234What “address already in use” actually means
A TCP port can have only one listening socket at a time. When your code calls server.listen(3000), the operating system checks whether anything is already bound to port 3000 in the LISTEN state. If so, the bind fails and libuv (the C library under Node) surfaces the OS-level EADDRINUSE as an 'error' event on the server object. Because most examples never attach an error handler, Node throws it as an uncaught exception and the process dies.
It helps to picture the socket lifecycle. A listening socket sits in LISTEN until you close it. Individual connections move through ESTABLISHED, then CLOSE_WAIT or TIME_WAIT as they wind down. Only a socket in LISTEN on your exact port and address blocks a new bind. That distinction matters, because it rules out the myth that a lingering closed connection is to blame.
Ninety percent of the time the culprit is one of these:
- A previous run never exited. You stopped the terminal but the process kept running, or a crash left an orphan behind. This is the single most common cause.
- Another tab or terminal is running the same server. Two
npm run devsessions both want port 3000. - A different app owns the port. Something unrelated is already on 3000, 5000, or 8080.
- Duplicate
listen()calls. The same server binds the port twice, often after a bad refactor or a hot-reload loop. - A parent and child both bind. Cluster or worker code where the primary and a worker both call
listenon the same fixed port. - A process manager restarted the app. PM2, systemd, or a Docker restart policy relaunched it before the old instance released the socket.
One thing it usually is not, at least on Linux and macOS: a leftover socket in TIME_WAIT. Node sets SO_REUSEADDR on listening sockets by default on those platforms, so a recently closed server does not block a fresh bind. If you get EADDRINUSE, a live process is almost certainly holding the port right now.
Which of these is your cause?
Before killing anything, spend five seconds matching the symptom to the cause. It tells you whether to hunt a process or fix your own code.
| Symptom | Most likely cause | Where to go |
|---|---|---|
Happens the second time you start npm run dev | Previous run never exited | Find and kill the process |
| Port you never used (5000, 7000 on macOS) | AirPlay Receiver owns it | See the AirPlay note below |
Fails only after nodemon restarts | Old server did not close in time | Add graceful shutdown |
| Fails in CI but not locally | Parallel test workers on a fixed port | Use port 0 or a per-worker port |
| Comes back seconds after you kill it | A process manager respawns it | Stop the service, not the PID |
Bind for 0.0.0.0:3000 failed | Docker host port already published | Stop the other container |
Below port 1024, says EACCES not EADDRINUSE | Missing privileges, not a busy port | Use a high port or grant capability |
Find the process holding the port (macOS and Linux)
Ask the OS which process owns the port. lsof is the most direct tool:
lsof -i :3000Output shows the command, PID, and user:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node 54321 you 22u IPv6 0xabc123 0t0 TCP *:3000 (LISTEN)That PID (54321) is what you kill. If the COMMAND column is truncated and you cannot tell which app it is, widen it with lsof +c0 -i :3000, which prints the full process name.
On Linux, ss is the modern replacement for the deprecated netstat and shows the process with -p:
ss -ltnp | grep :3000
# LISTEN 0 511 *:3000 *:* users:(("node",pid=54321,fd=22))The flags read as listening (-l), TCP (-t), numeric (-n), process (-p). Seeing a process owned by another user needs sudo. If you want to confirm nothing is stuck in TIME_WAIT either, drop -l and check every state: ss -tanp | grep :3000.
fuser prints and kills in one step:
fuser 3000/tcp # print the PID
fuser -k 3000/tcp # kill whatever owns the portOnce you have the PID, stop it gracefully first, then force it if it refuses:
kill 54321 # sends SIGTERM, lets the process clean up
kill -9 54321 # SIGKILL, only if it ignores SIGTERMFree the port on Windows
Windows uses different tools. Find the owning PID with netstat:
netstat -ano | findstr :3000 TCP 0.0.0.0:3000 0.0.0.0:0 LISTENING 54321Look at the row marked LISTENING. The last column is the PID. findstr :3000 also matches remote ports like ...:30000, so confirm you picked the LISTENING line, then kill it:
taskkill /PID 54321 /FPowerShell has a cleaner path that skips the manual PID copy:
Get-NetTCPConnection -LocalPort 3000 | Select-Object OwningProcess
Stop-Process -Id 54321 -ForceOr as a single command that finds and kills the owner:
Get-Process -Id (Get-NetTCPConnection -LocalPort 3000).OwningProcess | Stop-Process -ForceThe port tools, side by side
Every platform has two or three ways to do this. Keep one per OS in muscle memory:
| Task | macOS | Linux | Windows |
|---|---|---|---|
| Show who owns port 3000 | lsof -i :3000 | ss -ltnp | grep :3000 | netstat -ano | findstr :3000 |
| Print only the PID | lsof -ti:3000 | lsof -ti:3000 or fuser 3000/tcp | (Get-NetTCPConnection -LocalPort 3000).OwningProcess |
| Kill by PID | kill -9 <pid> | kill -9 <pid> | taskkill /PID <pid> /F |
| Find and kill in one line | lsof -ti:3000 | xargs kill -9 | fuser -k 3000/tcp | Stop-Process -Id (Get-NetTCPConnection -LocalPort 3000).OwningProcess -Force |
| Cross-platform, no setup | npx kill-port 3000 | npx kill-port 3000 | npx kill-port 3000 |
Free a port fast, on any OS
Skip the two-step dance with the kill-port package. No install needed:
npx kill-port 3000It works on macOS, Linux, and Windows, and takes multiple ports at once:
npx kill-port 3000 5173 8080For a Unix one-liner you can alias, combine lsof’s terse output with xargs:
lsof -ti:3000 | xargs kill -9-t makes lsof print only the PID, which pipes straight into kill. If nothing is on the port, add -r on GNU systems so xargs does not run kill with no arguments: lsof -ti:3000 | xargs -r kill -9.
Change the port instead of killing anything
Sometimes the process on 3000 is one you want to keep. Point your server at a free port instead. Most apps read process.env.PORT:
const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`Listening on ${port}`));Set it at launch. The syntax differs per shell:
# macOS / Linux (bash, zsh)
PORT=3001 node server.js# Windows PowerShell
$env:PORT=3001; node server.js:: Windows cmd.exe
set PORT=3001 && node server.jsFor a package.json script that runs everywhere, use cross-env so Windows and Unix behave the same:
npm install -D cross-env{
"scripts": {
"dev": "cross-env PORT=3001 node server.js"
}
}Handle EADDRINUSE gracefully in your code
An unhandled 'error' event is what turns a busy port into a stack trace. Attach a handler and print something useful instead of crashing:
const server = app.listen(port);
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`Port ${port} is already in use. Is another instance running?`);
process.exit(1);
} else {
throw err;
}
});If the port is likely to free up in a moment (a previous instance shutting down, a nodemon restart racing itself), retry with a short backoff instead of exiting:
function listenWithRetry(server, port, retries = 5, delay = 500) {
server.once('error', (err) => {
if (err.code === 'EADDRINUSE' && retries > 0) {
console.warn(`Port ${port} busy, retrying in ${delay}ms (${retries} left)`);
setTimeout(() => listenWithRetry(server, port, retries - 1, delay), delay);
} else {
throw err;
}
});
server.listen(port);
}The deeper fix is releasing the port cleanly when your process stops. A server killed with kill -9 or one that ignores termination signals holds the port until the OS reaps it, which is exactly what strands the port across restarts. Close the server on shutdown:
function shutdown() {
server.close(() => process.exit(0));
}
process.on('SIGINT', shutdown); // Ctrl+C
process.on('SIGTERM', shutdown); // docker stop, process managersThis matters most under nodemon and similar watchers. If a restart fires before the old server closes, the new one collides with the port the old one still owns.
The port stays busy after server.close()
server.close() stops the server from accepting new connections, but it does not cut the ones already open. HTTP keep-alive connections stay alive by design, so close() waits for them to idle out and the port stays bound the whole time. If you have ever called server.close() and still hit EADDRINUSE on the next start, this is why.
Node 18.2 added methods to force those sockets shut so the port releases immediately. Replace the shutdown above with this version:
function shutdown() {
server.close(() => process.exit(0)); // stop accepting new connections first
server.closeIdleConnections(); // drop keep-alive sockets sitting idle
setTimeout(() => server.closeAllConnections(), 5000); // force the rest after a grace period
}On older Node versions, the stoppable or http-terminator packages track open sockets and destroy them for you.
Auto-select a free port programmatically
For tools and tests, do not hardcode a port at all. Listening on port 0 tells the OS to hand you any free port, which you read back from server.address():
const server = app.listen(0, () => {
console.log(`Listening on ${server.address().port}`);
});When you need to know a port is free before you commit to it, probe with a throwaway server:
const net = require('net');
function isPortFree(port) {
return new Promise((resolve) => {
const tester = net.createServer()
.once('error', () => resolve(false))
.once('listening', () => tester.close(() => resolve(true)))
.listen(port);
});
}
// Walk up from a preferred port until one is free
async function findFreePort(start = 3000) {
let port = start;
while (!(await isPortFree(port))) port++;
return port;
}The get-port package does the same thing with edge cases handled (it avoids ports another call already reserved in the same process):
npm install get-portimport getPort from 'get-port';
const port = await getPort({ port: [3000, 3001, 3002] });When a process manager keeps grabbing the port
If the port frees for a second and then EADDRINUSE returns, something is restarting your app for you. Killing the PID is useless because the manager immediately spawns a replacement. Stop the service, not the process.
PM2: list, then delete or stop the app by name or id.
pm2 list
pm2 stop api # stop but keep it in the list
pm2 delete api # remove it entirelyA common trap is running the same server both under PM2 and directly with node server.js in another shell. PM2 cluster mode binds one shared port across workers correctly, but a stray node outside PM2 collides with it.
systemd: a unit with Restart=always respawns the instant you kill it. Stop the unit:
sudo systemctl stop myapp.service
sudo systemctl status myapp.service # confirm it is not restartingDocker: a container started with --restart always or unless-stopped comes back after docker kill. Find the container publishing the port and stop it:
docker ps --filter "publish=3000"
docker stop <container>EADDRINUSE in tests and CI
Tests are the most common place this error hides, because test runners spin many workers in parallel and each one may try to bind the same port. The cleanest fix is to never bind a real port in tests at all. With supertest, pass the Express or Koa app object directly and let supertest open an ephemeral port internally:
const request = require('supertest');
const app = require('../app'); // exports the app, does NOT call listen()
test('GET /health', async () => {
const res = await request(app).get('/health'); // no fixed port
expect(res.status).toBe(200);
});The key is separating app from server. Export the app from one file, and only call app.listen() in your entry point. Tests import the app, production imports the entry point.
If a test genuinely needs a live server, bind port 0 and read the assigned port, and always close it afterward:
let server;
beforeAll((done) => { server = app.listen(0, done); });
afterAll((done) => { server.close(done); });When you must use fixed ports across parallel Jest workers, derive a unique port per worker so they never collide:
const port = 3000 + Number(process.env.JEST_WORKER_ID || 1);Socket states and SO_REUSEADDR, explained
Understanding two socket options explains most of the surprising cases.
SO_REUSEADDR lets a new socket bind an address even when an old socket for a recently closed connection is still in TIME_WAIT. Node sets it by default on listening sockets on Linux and macOS, which is why a server you just restarted binds cleanly. Windows deliberately does the opposite: it does not set SO_REUSEADDR for Node’s listeners, because on Windows that option allows one process to hijack another’s port. So Windows tends to give you a stricter, more exclusive bind.
SO_REUSEPORT is a different feature. On Linux 3.9 and newer (and the BSDs) it lets multiple sockets bind the exact same port at once, and the kernel load-balances incoming connections across them. Node.js 22.12 and 23.1 added a reusePort listen option that exposes it, which is how you can run several worker processes each calling listen on port 3000 without an EADDRINUSE:
// Each worker process runs this; the kernel spreads connections
server.listen({ port: 3000, reusePort: true });Without reusePort, the correct multi-process pattern is the cluster module, where the primary owns the socket and passes connections to workers. Two workers each calling a plain listen(3000) will always collide.
Signals decide whether the port is released cleanly. SIGINT (Ctrl+C) and SIGTERM (docker stop, pm2 stop, kill) can be trapped, giving you the chance to run server.close(). SIGKILL (kill -9) cannot be trapped: the process dies instantly and the OS reclaims its sockets, but any child it spawned is orphaned and keeps its own sockets. That last point is the sharpest gotcha in dev: killing npm run dev does not kill the node child it launched, so the child keeps port 3000. Kill the actual node process, not the npm wrapper.
Beyond TCP: UDP sockets and Unix socket files
EADDRINUSE is not only an HTTP problem. Two other cases catch people out because the wording shifts from listen to bind.
UDP (dgram) sockets. Binding a UDP socket to a taken port raises bind EADDRINUSE:
const dgram = require('dgram');
const socket = dgram.createSocket('udp4');
socket.bind(41234); // throws EADDRINUSE if 41234 is takenFor multicast, or when several listeners legitimately share a port, opt into address reuse:
const socket = dgram.createSocket({ type: 'udp4', reuseAddr: true });Unix domain sockets. When you listen on a file path instead of a port, a stale socket file left by a crashed process causes EADDRINUSE even though nothing is running:
Error: listen EADDRINUSE: address already in use /tmp/app.sockA clean server.close() removes the file, but a hard crash skips that step and leaves it behind. Unlink the stale file on startup before you listen:
const fs = require('fs');
const path = '/tmp/app.sock';
try {
fs.unlinkSync(path);
} catch (err) {
if (err.code !== 'ENOENT') throw err; // ENOENT just means it was already gone
}
server.listen(path);Containers and Kubernetes
Inside a single container, EADDRINUSE behaves exactly as it does on a host. Orchestration adds two traps that do not exist on a laptop.
Two containers in one Kubernetes pod share a network namespace. If a sidecar and your app both listen on 3000, they collide as though they were one machine. Give each container a distinct containerPort.
hostPort collisions across pods. Two pods that both request the same hostPort on the same node cannot run together, and the second bind fails. Prefer a Service over hostPort unless you genuinely need host networking.
Compose restart policies keep respawning. A service with restart: always comes back the instant you kill it, so the port frees for a second and clashes again. Bring the whole stack down instead:
docker compose down
docker compose ps # confirm nothing is left runningFramework-specific behavior
Different dev servers react to a busy port differently:
- Next.js:
next devauto-increments and printsPort 3000 is in use, trying 3001 instead.next start(production) does not, so it throwsEADDRINUSE. Pass-p:next dev -p 3001. - Vite: defaults to auto-incrementing from 5173 unless you set
server.strictPort: true, which makes it fail loudly instead of silently moving. - Create React App: prompts
Something is already running on port 3000. Would you like to run on another port instead? - nodemon: does not change ports. A restart that races an un-closed server produces
EADDRINUSE. Graceful shutdown (above) fixes it, or set a smalldelayso the old process fully exits first. - Docker Compose: a clash on a
ports:mapping reads asBind for 0.0.0.0:3000 failed: port is already allocated. That is the host mapping, not Node. See Fix: Docker port is already allocated.
Still getting EADDRINUSE?
On macOS, ports 5000 and 7000 are taken by AirPlay. Since macOS Monterey, the AirPlay Receiver (the ControlCenter process) listens on port 5000, which is also Flask’s default. You will see EADDRINUSE on a port you never started anything on. Turn it off under System Settings → General → AirDrop & Handoff → AirPlay Receiver, or just use a different port.
Check for stray Node processes directly. If lsof shows nothing but the bind still fails, look for orphans:
ps aux | grep node # macOS / Linuxtasklist | findstr node # WindowsKill any you do not recognize, especially after an editor or debugger crash.
IPv4 versus IPv6 collisions. Node binds the IPv6 wildcard :: by default, which on a dual-stack system also claims IPv4. If another process bound only 0.0.0.0 (IPv4), your :: bind can still fail because it overlaps. Bind one family explicitly to test: app.listen(3000, '127.0.0.1').
On WSL2, the port may be held on the Windows side. WSL forwards localhost, so a Windows process on 3000 blocks a Linux bind and vice versa. Check both: netstat -ano | findstr :3000 in Windows and ss -ltnp | grep :3000 in WSL.
EACCES is a different error. Binding to a port below 1024 (like 80 or 443) as a non-root user gives EACCES: permission denied, not EADDRINUSE. The port is not busy, you lack permission. Use a high port behind a reverse proxy, or grant the capability with setcap 'cap_net_bind_service=+ep' $(which node) on Linux.
On Windows, a reserved range can block a port with nothing listening. Hyper-V, WSL2, and Docker Desktop reserve blocks of TCP ports, and if your dev port falls inside one, the bind fails even though netstat shows the port empty. List the reserved ranges:
netsh int ipv4 show excludedportrange protocol=tcpIf your port sits inside an excluded range, restart the NAT service to release the reservations, then start your server before something else claims the port:
net stop winnat
net start winnatOr exclude your specific port so Windows stops handing it out as a dynamic port: netsh int ipv4 add excludedportrange protocol=tcp startport=3000 numberofports=1.
Your fixed port may sit inside the ephemeral range. The OS draws temporary source ports for outbound connections from a range (32768 to 60999 on most Linux, seen with cat /proc/sys/net/ipv4/ip_local_port_range). Hardcode a listen port inside that range and an outbound connection can grab it first. Keep server ports below 32768 to stay clear.
Track down a duplicate listen() in your own code. If nothing external owns the port, your app may be binding it twice. Print the stack from the error handler to see the second caller, then grep for stray calls:
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') console.error(err.stack);
});grep -rn "\.listen(" src/The bind succeeds but requests still fail. If listen works yet clients cannot connect, the port was never the problem. That is usually a firewall, a wrong host binding (127.0.0.1 instead of 0.0.0.0 inside a container), or the server crashing right after startup.
Prevention checklist
A few habits stop this error from ever coming back:
- Export the
appand calllistenin exactly one entry file, never inside tests or shared modules. - Add
SIGINTandSIGTERMhandlers that callserver.close()so the port frees on every stop. - Use port
0orget-portin tests, and a per-worker port in parallel CI. - Read the port from
process.env.PORTso you can move it without touching code. - Kill the real
nodeprocess, not thenpmornodemonwrapper that spawned it. - On Windows, keep dev ports outside the Hyper-V reserved ranges.
For related runtime and networking errors, see Fix: ECONNREFUSED connect localhost, Fix: Node.js Unhandled Rejection Crash, Fix: EMFILE too many open files, and Fix: Next.js API route not working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.
Fix: Bun Not Working — Node.js Module Incompatible, Native Addon Fails, or bun test Errors
How to fix Bun runtime issues — Node.js API compatibility, native addons (node-gyp), Bun.serve vs Node http, bun test differences from Jest, and common package incompatibilities.
Fix: Node.js Stream Error — Pipe Not Working, Backpressure, or Premature Close
How to fix Node.js stream issues — pipe and pipeline errors, backpressure handling, Transform streams, async iteration, error propagation, and common stream anti-patterns.
Fix: Node.js UnhandledPromiseRejection and uncaughtException — Crashing Server
How to handle Node.js uncaughtException and unhandledRejection events — graceful shutdown, error logging, async error boundaries, and keeping servers alive safely.