TechEarl

Fix EADDRINUSE: Port Already in Use in Node.js

EADDRINUSE means another process already holds the port. Find it with lsof or netstat, kill it, or run npx kill-port. Plus the graceful in-code fix and the nodemon leftover-process trap.

Ishan Karunaratne⏱️ 10 min readUpdated
Share thisCopied
Fix the Node.js EADDRINUSE error: find the process holding the port with lsof or netstat, kill it, or use npx kill-port, plus the graceful server.on('error') handler.

EADDRINUSE means another process is already listening on the port your app is trying to bind. The fastest fix is to find that process and kill it, then restart your app.

On macOS or Linux:

bash
lsof -i :3000          # find the PID holding the port
kill -9 <pid>          # kill it (use the PID from the line above)

Or, with no manual PID copying, one command that works on any OS:

bash
npx kill-port 3000

On Windows:

bash
netstat -ano | findstr :3000   # the PID is the last column
taskkill /PID <pid> /F         # force-kill it

That clears the port. The rest of this page explains what the error actually is, the OS-by-OS commands in detail, the graceful way to handle it in code, and why nodemon and --watch leave a process behind in the first place.

What EADDRINUSE actually means

When a Node server calls server.listen(3000), it asks the OS for exclusive ownership of TCP port 3000 on a given address. If something else already holds it, the OS refuses and Node surfaces the error:

code
Error: listen EADDRINUSE: address already in use :::3000
    at Server.setupListenHandle [as _listen2] (node:net:1817:16)
    ...
  code: 'EADDRINUSE',
  errno: -48,
  syscall: 'listen',
  address: '::',
  port: 3000

EADDRINUSE is a POSIX system error (the errno differs by platform), not a Node-specific one. The cause is almost always one of three things:

  1. A previous run of your own app did not shut down and is still holding the port (the common case, especially after a crash or a killed-but-not-reaped terminal).
  2. A genuinely different program uses that port (another dev server, a database, a system service).
  3. You started two instances by accident (two terminals, or a watcher that spawned a duplicate).

So the fix is diagnostic first: find what holds the port, then decide whether to kill it or move your app to a different port.

Find and kill the process holding the port

The command differs by OS. Pick yours:

Try it with your own values

Pick your OS for the find-and-kill commands.

bash· Linux (GNU)
# 1. See exactly what is on the port:
lsof -i :3000
# (no lsof? use: ss -ltnp 'sport = :3000' )

# 2. Kill it by PID:
kill -9 <pid>

# Or one shot:
lsof -ti :3000 | xargs kill -9

A few notes on the macOS/Linux side. lsof -i :3000 lists every process with a connection on port 3000, and the PID column is what you feed to kill. This beats the old ps aux | grep node approach because it targets the port, not every Node process on the box, so you do not accidentally kill an unrelated server. lsof -ti :3000 prints only the PID (the -t flag), which is why piping it to xargs kill -9 frees the port in a single line. If lsof says nothing but the port is still busy, try sudo lsof -i :3000 (the owner may be another user) or ss -ltnp on Linux, where lsof is sometimes not installed.

On Windows, netstat -ano lists all connections with their owning PID in the last column; findstr :3000 filters to your port. Watch for LISTENING lines specifically, that is the bound socket. Then taskkill /PID <pid> /F forces it down (/F is required for most stubborn processes). In an elevated PowerShell you can do it in one line with Get-NetTCPConnection -LocalPort 3000, but the netstat/taskkill pair works in plain cmd.exe everywhere.

kill -9 (and taskkill /F) is a forced, no-cleanup termination. It is fine for a stuck dev server, but if it is your own app, a plain kill <pid> (SIGTERM) first lets it shut down gracefully and only escalate to -9 if it ignores you.

The OS-agnostic shortcut: npx kill-port

If you do not want to remember two different command sets, kill-port papers over the difference. It runs lsof/kill on Unix and netstat/taskkill on Windows under the hood:

bash
npx kill-port 3000
# multiple ports at once:
npx kill-port --port 3000,5173,8080

npx fetches and runs it without a global install. I keep it in a predev script so a stale process never blocks a restart:

json
{
  "scripts": {
    "predev": "kill-port 3000 || true",
    "dev": "node server.js"
  }
}

The || true keeps the script from failing when the port is already free.

The graceful fix: handle EADDRINUSE in code

Killing a process is the right move in development. In a long-running app you want the server to react to the error instead of crashing with an unhandled exception. server.listen() emits an 'error' event you can catch:

javascript
// Author: Ishan Karunaratne — https://techearl.com/fix-node-eaddrinuse-port-in-use
import { createServer } from "node:http";

const te_startServer = (port) => {
  const server = createServer((req, res) => {
    res.end("ok\n");
  });

  server.on("error", (err) => {
    if (err.code === "EADDRINUSE") {
      console.error(`Port ${port} is in use.`);
      process.exit(1); // or: retry on the next port (below)
    } else {
      throw err; // anything else is a real bug, do not swallow it
    }
  });

  server.listen(port, () => {
    console.log(`Listening on http://localhost:${port}`);
  });

  return server;
};

te_startServer(Number(process.env.PORT) || 3000);

Two things matter here. First, only special-case EADDRINUSE; re-throw every other error code so you do not silently hide a real failure (a permission error on a low port, for instance, shows up as EACCES, not EADDRINUSE). Second, decide deliberately between exiting and retrying. A web server in production should exit with a non-zero code and let your process manager (systemd, PM2, a container orchestrator) handle the restart. A local tool that just needs a port can hunt for the next free one:

javascript
// Author: Ishan Karunaratne — https://techearl.com/fix-node-eaddrinuse-port-in-use
const te_listenWithRetry = (server, port, attemptsLeft = 10) => {
  server.once("error", (err) => {
    if (err.code === "EADDRINUSE" && attemptsLeft > 0) {
      console.warn(`Port ${port} busy, trying ${port + 1}...`);
      te_listenWithRetry(server, port + 1, attemptsLeft - 1);
    } else {
      throw err;
    }
  });
  server.listen(port);
};

Reading the port from an environment variable (process.env.PORT) instead of hard-coding it is the cleaner long-term answer, because it lets you sidestep a collision without editing code. See reading environment variables in Node.js for the --env-file and process.env patterns.

One thing people reach for that does not help here: SO_REUSEADDR. Node sets that socket option on listening sockets already, and it only governs reusing a socket stuck in TIME_WAIT, not a port another live process is actively listening on. EADDRINUSE from a running server is not something a socket flag will fix; you have to free the port.

Why nodemon and --watch leave a process behind

The single most common source of EADDRINUSE in development is a leftover process from a watcher. nodemon and node --watch restart your app on every file save by killing the old process and spawning a new one. If your code spawns a child (a worker, a second server, a child_process) and does not forward the kill signal to it, the watcher kills the parent but the child keeps holding the port. The next restart then hits EADDRINUSE.

The fixes:

  • Trap the signal and close cleanly so a restart actually releases the port:

    javascript
    // Author: Ishan Karunaratne — https://techearl.com/fix-node-eaddrinuse-port-in-use
    const te_shutdown = () => server.close(() => process.exit(0));
    process.on("SIGTERM", te_shutdown);
    process.on("SIGINT", te_shutdown);
  • Kill children when the parent dies. If you spawn workers, listen for the parent's exit and tear them down, or detach them properly. Orphaned children that bind ports are exactly this bug. See catching errors from Node.js child_process for handling spawned-process lifecycles.

  • Free the port on restart with the predev/kill-port trick above, which makes the watcher resilient to a child it failed to reap.

If switching Node versions is part of how you got here (an old global server from a previous install still running), updating your Node.js version cleanly is worth doing so you are not juggling multiple runtimes.

Avoid the collision entirely: a per-project port

If you run several projects at once, hard-coding 3000 everywhere guarantees collisions. A neat trick is to derive a stable port from the project path, so each checkout gets its own port without you tracking them:

javascript
// Author: Ishan Karunaratne — https://techearl.com/fix-node-eaddrinuse-port-in-use
import { createHash } from "node:crypto";

const te_portForPath = (dir = process.cwd()) => {
  const hash = createHash("sha1").update(dir).digest("hex");
  // map into the 3000-3999 range
  return 3000 + (parseInt(hash.slice(0, 8), 16) % 1000);
};

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

Two different project directories hash to two different ports, the same directory always gets the same port, and process.env.PORT still wins when you set it. It is a small thing that removes a whole class of "which app is on 3000 again?" friction. Promises and async startup tie into this too; if you await something before listen(), make sure errors there are handled (see the JavaScript promises guide).

FAQ

See also

Sources

Authoritative references this article was fact-checked against.

TagsEADDRINUSENode.jsport in uselsofkill-portnetstatnodemonerror handling

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

Fix NODE_MODULE_VERSION Mismatch in Node.js

The NODE_MODULE_VERSION mismatch error means a native addon was compiled against a different Node ABI than the one now running it. Here is what the numbers mean, the one-line fix, and the Electron, Docker, and CI variants that catch people out.