CoreMesh

EADDRINUSE Address Already in Use: Find and Stop the Process

Fix Error: listen EADDRINUSE: address already in use. Find and stop the process on the port with lsof, ss, netstat or taskkill, or switch ports in Node.

EADDRINUSE Address Already in Use: Find and Stop the Process

EADDRINUSE means Node asked the operating system for a listening socket on a port, and something on the same machine already holds that address and port. The Node.js errors reference describes it as a failed attempt to bind a net, http or https server because another server already occupies the address. There are two fixes: stop whatever owns the port, or start your app on a different one.

Most of the time the owner is an earlier copy of your own app. A dev server in a forgotten terminal tab, a file watcher that restarted but left its child alive, or a container still publishing the port. The commands below find it on macOS, Linux and Windows. The sections after that cover switching ports and handling the error in code so it stops crashing your process.

What the error looks like and why it happens#

The full crash usually reads like this (line numbers vary by Node version):

text
node:events:497
      throw er; // Unhandled 'error' event
      ^

Error: listen EADDRINUSE: address already in use :::3000
    at Server.setupListenHandle [as _listen2] (node:net:1908:16)
    at listenInCluster (node:net:1965:12)
    at Server.listen (node:net:2067:7)
Emitted 'error' event on Server instance at:
    at emitErrorNT (node:net:1944:8)
    at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
  code: 'EADDRINUSE',
  errno: -98,
  syscall: 'listen',
  address: '::',
  port: 3000
}

Three fields on that error object do the diagnosing for you. syscall: 'listen' says it failed while setting up a server, not while connecting out. address is the interface Node tried to bind. :: is the unspecified IPv6 address, which Node uses when you don't pass a host, and on most systems it covers IPv4 as well. port is the number that's taken. Don't key logic off the numeric errno value. Compare err.code === 'EADDRINUSE' instead.

The process dies because server.listen() emitted an 'error' event and nothing was subscribed to it. That's what the Unhandled 'error' event line at the top means. The port conflict is the cause, but the missing handler is what turned it into a crash.

One detail saves a lot of head-scratching. Node sets SO_REUSEADDR on its sockets, so connections lingering in TIME_WAIT from a previous run don't normally trigger this error. A live listener does. If you see EADDRINUSE, assume a real process is sitting on the port and go find it.

Find and stop the process on macOS and Linux#

lsof works on both systems and prints the command name, PID and user on one line. Replace 3000 with your port:

bash
lsof -nP -iTCP:3000 -sTCP:LISTEN
text
COMMAND   PID  USER   FD   TYPE  DEVICE SIZE/OFF NODE NAME
node    48213  indra  23u  IPv6  0x1a2b      0t0  TCP *:3000 (LISTEN)

-nP skips DNS and service-name lookups so the output is fast and shows raw numbers. -sTCP:LISTEN drops client connections to that port, which is what you want, because only the listener matters. If nothing prints, the port is free as far as this host can see, and you should check the gotchas section for the Docker, WSL and IPv4 versus IPv6 cases.

On Linux, ss is usually faster and installed by default:

bash
ss -ltnp | grep ':3000'
text
LISTEN 0 511  *:3000  *:*  users:(("node",pid=48213,fd=23))

The p flag only shows PIDs for processes you own. Prefix the command with sudo if the users part is missing. The ss man page covers the filter syntax if you'd rather match a port without grep.

Before you kill anything, check what the PID actually is. node could be any Node app on the machine:

bash
ps -p 48213 -o pid=,args=

Now stop it. Start with a plain kill, which sends SIGTERM and lets the process shut down cleanly:

bash
kill 48213

Re-run the lsof or ss command. If the line is gone, start your app and you're done. If the process is still there after a few seconds, it's ignoring SIGTERM, and you can escalate:

bash
kill -9 48213

Treat kill -9 as the last resort. It skips every cleanup handler, so a database writer or a test runner can leave lock files or half-written state behind. Confirm the PID really is the stray server before you send it.

A one-liner you'll see everywhere is kill $(lsof -t -i :3000). It works, but -t prints every PID touching the port, including clients connected to it, so on a busy port it can kill more than you intended. I'd look at the full list first and kill by number.

Find and stop the process on Windows#

In Command Prompt, netstat lists listeners with their PIDs and findstr narrows the output to your port:

bash
netstat -ano | findstr :3000
text
TCP    0.0.0.0:3000    0.0.0.0:0    LISTENING    17340
TCP    [::]:3000       [::]:0       LISTENING    17340

The last column is the PID. To see what it is before you kill it:

bash
tasklist /FI "PID eq 17340"

Then stop it:

bash
taskkill /PID 17340 /F

/F forces termination. Without it, taskkill only asks the process to close, and a console program such as node.exe usually ignores the request, so in practice you need the flag. Re-run the netstat line to confirm the LISTENING entry is gone.

In PowerShell the same steps give you the process name directly:

powershell
Get-NetTCPConnection -LocalPort 3000 -State Listen | Select-Object OwningProcess
Get-Process -Id 17340
Stop-Process -Id 17340

If the owning process is svchost.exe or System, a Windows service or a reserved port range holds it. Don't kill those. Switch ports instead.

Pick another port instead#

Sometimes killing isn't an option. The other process is a colleague's service in a shared VM, a system daemon, or a container you actually need. Then move your app.

Read the port from the environment with a sensible default:

javascript
const http = require('node:http');

const port = Number(process.env.PORT) || 3000;

http.createServer((req, res) => {
  res.end('ok');
}).listen(port, () => {
  console.log(`listening on ${port}`);
});
bash
PORT=3001 node server.js

In Windows Command Prompt, set the variable on its own line first:

bash
set PORT=3001
node server.js

Most framework dev servers read PORT or take a port option on the command line. Check your dev command's --help output rather than guessing.

If you don't care which port you get, pass 0. The server.listen documentation says that with a port of 0 the OS assigns an arbitrary unused port, and you read it back from server.address().port after the 'listening' event. That's ideal for integration tests, which otherwise fight each other over a hard-coded port.

Once your API moves to a new port, the browser treats it as a different origin. If requests from your front end suddenly fail, read which header is missing when you hit a CORS error before assuming the port change broke something else.

Handle EADDRINUSE inside your Node app#

A listen failure is an 'error' event on the server. Subscribe to it and the crash turns into a message you control. This example takes a port, deliberately collides with it, and prints the fields you'd want in a log line:

javascript
const net = require('node:net');

const first = net.createServer();

first.listen(0, '127.0.0.1', () => {
  const { port } = first.address();
  console.log(`first server is listening on 127.0.0.1:${port}`);

  const second = net.createServer();

  second.on('error', (err) => {
    console.log('second server failed to listen');
    console.log('  code:    ' + err.code);
    console.log('  syscall: ' + err.syscall);
    console.log('  address: ' + err.address);
    console.log('  port:    ' + err.port);
    console.log('  message: ' + err.message);
    first.close(() => console.log('first server closed, port released'));
  });

  second.listen(port, '127.0.0.1');
});

The pattern Node's own docs suggest for transient conflicts is to retry after a delay:

javascript
server.on('error', (e) => {
  if (e.code === 'EADDRINUSE') {
    console.error('Address in use, retrying...');
    setTimeout(() => {
      server.close();
      server.listen(PORT, HOST);
    }, 1000);
  }
});

That helps when a previous instance is still shutting down, as in a rolling restart. It won't help against a process that's going to keep the port forever, so cap the retries or combine them with a fallback. Here's a version that tries the preferred port and falls back to an OS-assigned one:

javascript
const net = require('node:net');

// Occupy a random free port so the demo collides on purpose.
const blocker = net.createServer();

blocker.listen(0, '127.0.0.1', () => {
  const wanted = blocker.address().port;
  const app = net.createServer();

  app.once('error', (err) => {
    if (err.code !== 'EADDRINUSE') throw err;
    console.log(`port ${wanted} is busy, asking the OS for a free one`);
    app.listen(0, '127.0.0.1');
  });

  app.on('listening', () => {
    console.log(`app is listening on ${app.address().port}`);
    app.close();
    blocker.close();
  });

  app.listen(wanted, '127.0.0.1');
});

Two rules from the docs matter here. server.listen() may be called again only after an error or after server.close(). Calling it on a server that's already listening throws ERR_SERVER_ALREADY_LISTEN, which is a bug in your code rather than a port conflict. And server.close() is asynchronous. It stops accepting new connections and emits 'close' once existing ones end, so the port isn't necessarily free the instant you call it.

Which process usually owns ports 3000, 5000, 8080 and 443#

The port number in the error is a strong hint about the culprit.

Port Usual owner What to do
3000 A second copy of the same Node dev server in another terminal, or a leftover child of a file watcher Kill the stray copy, or run with PORT=3001
3001 The app that already fell back from 3000 once, or a second project you forgot about Same as above
4000 Another API or GraphQL dev server Move one of them
5000 On macOS, the built-in AirPlay Receiver listens here. Flask also defaults to it Turn off AirPlay Receiver in System Settings under General and AirDrop & Handoff, or use another port
8080 Alternate HTTP: proxies, Java app servers, a Docker container publishing 8080 Run docker ps to check containers, then stop or remap
8081 React Native's Metro bundler Stop the old Metro session, or start it on a different port
443 A real web server (nginx, Apache, IIS) or a container publishing 443 Don't kill it blindly. Proxy to your app or use 8443 locally

On Linux, ports below 1024 need elevated privileges, so an unprivileged bind to 443 fails with EACCES rather than EADDRINUSE. If you do see EADDRINUSE on 443, you were already running with enough rights and something real is serving HTTPS there.

Common mistakes and gotchas#

  • Killing the child of a watcher. Tools like nodemon restart the child process when it dies, so killing the node PID just makes the watcher spawn a fresh one that grabs the port again. Find the terminal running the watcher and stop it there with Ctrl+C, or kill the parent PID.
  • Killing a Docker proxy instead of the container. When a container publishes a port, lsof shows a Docker helper process as the owner. Killing it leaves the container running and Docker will often put the proxy back. Use docker ps and docker stop on the container. If Docker commands themselves fail for you, the Docker socket permission fix is usually the reason.
  • Looking on the wrong side of WSL. On Windows with WSL 2, a server started inside the Linux VM is forwarded to Windows, and vice versa can appear busy from inside. Run ss inside WSL and netstat -ano in Windows before concluding the port is free.
  • Mixing up IPv4 and IPv6. Node binds :: by default, which on most systems also claims 0.0.0.0. A process bound to 127.0.0.1:3000 or 0.0.0.0:3000 can therefore block you even though your error says :::3000. lsof -iTCP:3000 shows both families, so trust it over a search for the literal ::.
  • Calling listen twice. ERR_SERVER_ALREADY_LISTEN looks related but isn't. It means your code called server.listen() on a server that's already up, often from a module that gets imported twice.
  • Reaching for reusePort. The reusePort option on server.listen() lets multiple sockets deliberately share a port with the OS distributing connections between them. It's supported only on some platforms (Linux 3.9+, FreeBSD 12.0+ and a few others) and raises an error elsewhere. It's a load-sharing feature, not a way to paper over a stray process.
  • Cluster workers fighting over a handle. In cluster mode, workers share the listening handle by default. Setting exclusive: true stops that sharing, and a second worker then fails with a port error. Leave it at the default unless you specifically want separate handles.
  • Expecting close() to free the port instantly. server.close() waits for existing connections to end. A restart script that calls it and immediately starts a new listener can hit EADDRINUSE from its own previous instance. Wait for the 'close' event.

Quick reference#

Work down this list and stop at the first step that fixes it.

  1. Read port and address from the error object, not the stack trace.
  2. Find the owner. macOS and Linux: lsof -nP -iTCP:3000 -sTCP:LISTEN. Linux: ss -ltnp | grep ':3000'. Windows: netstat -ano | findstr :3000.
  3. Identify it before killing: ps -p PID -o args= or tasklist /FI "PID eq PID".
  4. Stop it gently: kill PID or taskkill /PID PID /F. Then re-run step 2 to confirm.
  5. Only if it survives: kill -9 PID, after confirming it's a stray dev process and not a database or system service.
  6. If you can't or shouldn't kill it, run on another port: PORT=3001 node server.js, or pass 0 and read server.address().port.
  7. Add an 'error' listener on the server so the next conflict logs a message instead of crashing.
  8. Watchers, Docker containers and WSL are the usual reasons the port looks free but isn't.

Quick answers

Why do I get EADDRINUSE on port 3000 every time I restart my dev server?

An earlier instance is almost always still alive, often a child process a file watcher left behind or a server in another terminal tab. Run lsof -nP -iTCP:3000 -sTCP:LISTEN (or netstat -ano | findstr :3000 on Windows), kill that PID, and start again.

Port 5000 is in use on my Mac but I'm not running anything. What has it?

On recent macOS versions the built-in AirPlay Receiver listens on port 5000. Turn it off in System Settings under General and AirDrop & Handoff, or start your app on a different port.

What is holding port 8081?

On a React Native machine it's usually the Metro bundler from an earlier session. Stop that terminal or kill the PID that lsof or netstat reports, or start Metro on a different port.

Can I just use a different port instead of killing the process?

Yes. Read the port from an environment variable such as PORT and start with PORT=3001, or pass 0 to server.listen() and the OS assigns a free port you can read from server.address().port.

Does address already in use on port 443 need a different fix?

The cause is the same, but the owner is usually a real web server or a container publishing 443, so don't kill it blindly. On Linux, binding below 1024 as a regular user fails with EACCES instead, so EADDRINUSE there means you had privileges and something is genuinely serving HTTPS.

Did this solve it?

One tap. No account.

Saved you an hour? Send a tip.

USDC or ETH on Base, Ethereum, Arbitrum and more. Keeps the notes free and the examples running.

Base

Network

0xc783bc5c3fa14cEC3f09A22284bC91C5AF0d59e1
Open in wallet · 5 USDC

On a computer? Scan the code with your phone wallet, or copy the address.

Pick the network your wallet or exchange uses. USDC works on all six; ETH on Base, Ethereum, Arbitrum and Optimism. Don't send on Tron or Solana: this address doesn't exist there. Tips are optional and unlock nothing; every guide stays free.

Discussion

Start the discussion

Indra Wijaya

Builds and runs AI agents, bots and content pipelines, and writes down what works and what broke.

About · Email · RSS

Keep reading

New notes, no spam.

One post at a time, only when there's something worth reading. Follow in any feed reader.

Subscribe via RSS