Error: listen EADDRINUSE: address already in use :::3000 - 10 Causes and Fixes
You started your server and got Error: listen EADDRINUSE: address already in use :::3000. It means exactly what it says: something already has port 3000 bound, and the operating system refused to hand it to your process.
The :::3000 part is not a typo. Those three colons are an abbreviated IPv6 wildcard address (::) followed by the port, so your server asked for every interface on port 3000 and lost.
Ninety percent of the time the culprit is your own previous run that never fully exited. The rest of the time it is a second copy of your app, a Docker publish rule, a dev server with hot reload, a system service, or a bug in your own code that calls listen() twice.
This page gives you the one-line fix first, then ten distinct causes with reproductions, then a checklist for the cases that do not match any of them.
| Error | Error: listen EADDRINUSE: address already in use :::3000 |
|---|---|
| Where it happens | Node.js (any version) - Express, Fastify, Next.js, NestJS, Vite, webpack-dev-server, or a raw net/http server. The same EADDRINUSE errno surfaces in Python, Go, Java and nginx with different wording. |
| What it means | Your process asked the OS to bind TCP port 3000 on all interfaces, and another socket is already holding that address. |
The Fast Fix
Find whatever owns port 3000 and kill it.
# macOS / Linux
lsof -ti :3000 | xargs kill -9
# Windows (PowerShell)
Get-NetTCPConnection -LocalPort 3000 -State Listen |
Select-Object -ExpandProperty OwningProcess |
ForEach-Object { Stop-Process -Id $_ -Force }
Then start your server again. If the error comes back immediately, something is restarting that process (a supervisor, docker compose, pm2, a VS Code task) - stop that instead, or just run on another port:
PORT=3001 npm start
If PORT does nothing, your code hardcodes the port - see cause 8.
What Is Actually Causing It
Jump to your case
- 1. A previous run of your own server is still alive
- 2. Two terminal tabs running the same dev command
- 3. The process is a zombie child of a crashed watcher
- 4. A Docker container publishes port 3000 to the host
- 5. A different app on your machine owns port 3000
- 6. Your code calls listen() more than once
- 7. A test suite starts a server per test file
- 8. The PORT environment variable is ignored by your code
- 9. A process manager restarted the app behind your back
- 10. A reverse proxy or tunnel is bound to 3000
- 11. The port is held in TIME_WAIT by a socket that just closed
- 12. A WSL, VM, or host-side process shares the port namespace
1. A previous run of your own server is still alive
Reproduce it
$ node server.js &
[1] 48219
$ node server.js
Error: listen EADDRINUSE: address already in use :::3000
Why it happens — You backgrounded, detached, or lost the terminal for an earlier run, or you closed the terminal tab without the shell forwarding a signal. The process kept running and kept the listening socket. The OS will not let a second socket bind the same address and port.
The fix
# See exactly which PID holds it, then kill that PID
lsof -i :3000 # macOS/Linux: note the PID column
kill 48219 # try graceful first
kill -9 48219 # only if it ignores SIGTERM
What changed: you are terminating the old listener instead of starting a second one. Prefer kill over kill -9 so the process runs its shutdown handlers.
Confirm it worked — lsof -i :3000 prints nothing (exit code 1), and node server.js boots without the error.
2. Two terminal tabs running the same dev command
Reproduce it
# Tab A
$ npm run dev
ready - started server on 0.0.0.0:3000
# Tab B (same repo, forgotten)
$ npm run dev
Error: listen EADDRINUSE: address already in use :::3000
Why it happens — Both commands are healthy processes doing the right thing; the second one just lost the race for the port. This is the most common false alarm, because the first server is working fine in a tab you cannot see.
The fix
# Confirm the owner is a node process started from your project
ps -p "$(lsof -ti :3000)" -o pid,command
If it is your dev server, switch to that tab and reuse it. Nothing to fix in code - what changed is that you stopped starting a duplicate.
Confirm it worked — Open http://localhost:3000 in a browser. If your app answers, the existing server is the one you wanted and no fix is needed.
3. The process is a zombie child of a crashed watcher
Reproduce it
$ npx nodemon server.js
# crash the watcher with a hard kill, not Ctrl-C
$ kill -9 $(pgrep -f "nodemon server.js")
$ node server.js
Error: listen EADDRINUSE: address already in use :::3000
Why it happens — nodemon, concurrently, tsx watch and turbo spawn your server as a child process. kill -9 on the parent never reaches the child, so the child is reparented and keeps the socket open with no visible owner in your shell.
The fix
# Kill the whole process group instead of a single PID
kill -- -$(ps -o pgid= $(lsof -ti :3000) | tr -d ' ')
What changed: the negative PID targets the process group, so the orphaned child dies with its siblings. For future runs, stop watchers with Ctrl-C so they can signal their children.
Confirm it worked — pgrep -f node no longer lists a server process from your project, and lsof -i :3000 is empty.
4. A Docker container publishes port 3000 to the host
Reproduce it
$ docker run -d -p 3000:3000 myapp
9f2c1ab3d4e5
$ node server.js
Error: listen EADDRINUSE: address already in use :::3000
Why it happens — -p 3000:3000 makes the Docker daemon (or docker-proxy) bind host port 3000 on your behalf. The host socket belongs to Docker, not to your shell, so lsof shows docker or com.docker.backend rather than node.
The fix
# Find the container holding the host port
docker ps --format '{{.ID}}\t{{.Image}}\t{{.Ports}}' | grep 3000
docker stop 9f2c1ab3d4e5
# Or publish on a different host port and leave 3000 to your local run
docker run -d -p 3001:3000 myapp
What changed: the host side of the publish mapping moved to 3001. The container still listens on 3000 internally.
Confirm it worked — docker ps shows no 0.0.0.0:3000-> mapping, or curl localhost:3001 reaches the container while localhost:3000 is free.
5. A different app on your machine owns port 3000
Reproduce it
$ lsof -i :3000
COMMAND PID USER FD TYPE DEVICE NODE NAME
Grafana 7712 g1 9u IPv6 0x9a1f TCP *:3000 (LISTEN)
Why it happens — Port 3000 is the default for a surprising number of tools - Grafana, Rails' bin/dev setups, Meteor, Gitea, Sentry's local stack. If one of them is installed as a background service, it grabs 3000 at login before you ever open a terminal.
The fix
# macOS: stop a Homebrew-managed service for good
brew services stop grafana
# Linux: stop and disable a systemd unit
sudo systemctl stop grafana-server
sudo systemctl disable grafana-server
What changed: disable stops it coming back on the next boot, which stop alone does not do.
Confirm it worked — Reboot (or log out and back in) and run lsof -i :3000 before starting anything - it should be empty.
6. Your code calls listen() more than once
Reproduce it
// server.js
const express = require('express');
const app = express();
app.listen(3000);
// later in the same file, or in a second require of this module
app.listen(3000); // second bind on the same port
// Error: listen EADDRINUSE: address already in use :::3000
Why it happens — Each listen() creates a new socket. The second one tries to bind an address the first one in the same process already holds, so the OS rejects it. This often hides behind a module that is imported twice under two different specifiers, or a listen() inside a function you call in a loop.
The fix
// server.js
const express = require('express');
const app = express();
module.exports = app; // export, do not listen
// start.js - the only place that binds
const app = require('./server');
const server = app.listen(process.env.PORT || 3000);
module.exports = server;
What changed: binding happens in exactly one entry point, so importing the app for tests or scripts no longer opens a socket.
Confirm it worked — Add console.log('listening', server.address()) after the single listen() call. It must print once per process start, not twice.
More causes (6 remaining)
7. A test suite starts a server per test file
Reproduce it
// users.test.js and orders.test.js both do this
const app = require('../server');
beforeAll(() => app.listen(3000));
// second file: Error: listen EADDRINUSE: address already in use :::3000
Why it happens — Jest, Vitest and Mocha can run test files in parallel workers. Two workers hit listen(3000) at the same time and one loses. Even serial runs fail if the first suite never closes its server.
The fix
// Bind to port 0 - the OS assigns a free ephemeral port
let server;
beforeAll(() => { server = app.listen(0); });
afterAll(() => new Promise(done => server.close(done)));
// Read the real port when you need to build a URL
const url = () => `http://127.0.0.1:${server.address().port}`;
What changed: listen(0) plus an afterAll close, so no two workers compete and no socket leaks between files. With supertest you can skip listen() entirely and pass app directly.
Confirm it worked — Run the suite twice in a row without killing anything in between. Both runs pass, and lsof -i :3000 is empty afterwards.
8. The PORT environment variable is ignored by your code
Reproduce it
// server.js
app.listen(3000); // hardcoded
$ PORT=3001 node server.js
Error: listen EADDRINUSE: address already in use :::3000
Why it happens — You tried to dodge the conflict with PORT=3001, but the literal 3000 in the source wins. The error message still says :::3000, which makes it look like the variable was overridden by something else.
The fix
const port = Number(process.env.PORT) || 3000;
app.listen(port, () => console.log(`listening on ${port}`));
What changed: the port is read from the environment with 3000 as a fallback. Note that some frameworks need a CLI flag instead - next dev -p 3001, vite --port 3001.
Confirm it worked — PORT=3001 node server.js logs listening on 3001, and curl localhost:3001 answers.
9. A process manager restarted the app behind your back
Reproduce it
$ pm2 start server.js --name api
$ node server.js
Error: listen EADDRINUSE: address already in use :::3000
Why it happens — pm2, systemd, forever and supervisord exist to keep a process alive. Killing the PID that holds port 3000 just makes the supervisor start a fresh one, so the port is reoccupied within milliseconds and the error looks unkillable.
The fix
# Stop at the supervisor level, not the PID level
pm2 stop api
pm2 delete api # so it does not resurrect on `pm2 resurrect`
# systemd equivalent
sudo systemctl stop my-api
What changed: you removed the thing that was respawning the listener instead of fighting its children.
Confirm it worked — pm2 list shows the app stopped (or gone), and lsof -i :3000 stays empty for several seconds after you check it twice.
10. A reverse proxy or tunnel is bound to 3000
Reproduce it
$ ngrok http --url-scheme http 3000 &
# or a local nginx with: listen 3000;
$ node server.js
Error: listen EADDRINUSE: address already in use :::3000
Why it happens — Most tunnels only make outbound connections and do not bind the local port, but a local proxy configured to listen on 3000 (nginx, Caddy, Traefik, a Kubernetes port-forward) does. kubectl port-forward in particular binds the local side and survives in a backgrounded shell job.
The fix
# Identify the owner by command name before guessing
ps -p "$(lsof -ti :3000)" -o pid,comm
# Kubernetes port-forward: stop the job that holds it
jobs # find the port-forward job number
kill %1
# nginx: change the listen directive, then reload
# listen 3000; -> listen 3100;
sudo nginx -s reload
What changed: the proxy's listen port moved, or the forwarding job was stopped, freeing 3000 for your app.
Confirm it worked — ps -p "$(lsof -ti :3000)" -o comm errors out because lsof returned nothing, meaning the port is free.
11. The port is held in TIME_WAIT by a socket that just closed
Reproduce it
// Restarting a server with live connections immediately after SIGINT
// can fail on some configurations:
Error: listen EADDRINUSE: address already in use :::3000
$ netstat -an | grep 3000
tcp46 0 0 *.3000 *.* TIME_WAIT
Why it happens — TCP keeps a closed socket's address reserved briefly so late packets from the old connection are not delivered to a new one. Node sets SO_REUSEADDR on listening sockets by default, so this is rare - but it does happen when the old process is still finishing its close, or when another socket is in a non-reusable state.
The fix
// Close listeners and connections on shutdown so the socket is released cleanly
const server = app.listen(port);
for (const sig of ['SIGINT', 'SIGTERM']) {
process.on(sig, () => {
server.close(() => process.exit(0));
});
}
What changed: an explicit server.close() on signal, so the listener is torn down instead of being killed mid-flight. If you only need to get moving, waiting a few seconds or using a different port also works.
Confirm it worked — netstat -an | grep 3000 shows no entry (not even TIME_WAIT), then the restart succeeds.
12. A WSL, VM, or host-side process shares the port namespace
Reproduce it
# Inside WSL2
$ node server.js
Error: listen EADDRINUSE: address already in use :::3000
$ lsof -i :3000
# prints nothing - no Linux process owns it
Why it happens — WSL2 forwards localhost between Windows and Linux, so a Windows process listening on 3000 can block a bind inside WSL and vice versa. The Linux-side lsof cannot see the Windows owner, which is why the port looks free and still refuses to bind.
The fix
# Run this on the Windows side, not inside WSL
Get-NetTCPConnection -LocalPort 3000 -State Listen |
Select-Object LocalAddress, OwningProcess,
@{n='Name';e={(Get-Process -Id $_.OwningProcess).ProcessName}}
Then stop that Windows process, or bind explicitly to the WSL interface instead of the wildcard:
app.listen(3000, '0.0.0.0');
What changed: you looked for the owner in the other namespace, where it actually lives.
Confirm it worked — The PowerShell command returns no rows for port 3000, and the bind inside WSL succeeds.
## None of Those? Narrow It Down
- Run
lsof -i :3000(macOS/Linux) orGet-NetTCPConnection -LocalPort 3000 -State Listen(Windows). If it names a PID, the conflict is a live local listener - go to step 2. If it prints nothing, skip to step 6. - Print the full command line of that PID:
ps -p <PID> -o pid,ppid,command. Anode/next/vitecommand from your own project means a duplicate run (causes 1-3). Anything else means a foreign app (cause 5) or a proxy (cause 10). - Check the parent PID from step 2. If the parent is
pm2,systemd,supervisord, ordocker, a supervisor will respawn it - stop it at that level (causes 4, 9) rather than killing the child. - Kill the PID, wait two seconds, then re-run the step 1 command. If the port is occupied again by a new PID, something is restarting it - confirm with
pm2 listanddocker ps. - If the port is now free but your app still fails, grep your source for a hardcoded port:
grep -rn "3000" src/ server.js. A literallisten(3000)meansPORTis being ignored (cause 8), and two matches mean a double bind (cause 6). - If nothing owns the port, check
netstat -an | grep 3000for aTIME_WAITorCLOSE_WAITentry (cause 11), and checkdocker psfor a0.0.0.0:3000->mapping, since Docker's host binding does not always show as a plain node process. - If you are in WSL, a VM, or a container, repeat step 1 on the host side as well - a bind can be blocked by a listener your current namespace cannot see (cause 12).
- As a final isolation test, run
node -e "require('net').createServer().listen(3000, () => console.log('bound'))". If that printsbound, the port is genuinely free and the fault is inside your application's startup code, not the environment.
Why This Error Exists At All
EADDRINUSE is not a Node.js error. It is a POSIX errno returned by the bind() system call, and Node simply wraps it in a JavaScript Error with the syscall name attached. That is why the message reads listen EADDRINUSE - listen is the Node operation, EADDRINUSE is the kernel's verdict. Python raises OSError: [Errno 98] Address already in use for the identical condition; Go returns bind: address already in use. Same kernel refusal, three wordings.
The refusal exists because a TCP listening socket is identified by the tuple (protocol, local address, local port), and that tuple has to be unique. If two processes could both listen on 0.0.0.0:3000, the kernel would have no deterministic rule for deciding which one receives an incoming SYN packet. Rather than pick arbitrarily, the OS makes the second bind() fail. The :::3000 in your message is that tuple printed back at you: :: is the IPv6 any-address, and on most systems an IPv6 wildcard bind also covers IPv4, which is why binding [::]:3000 collides with a process listening on 0.0.0.0:3000.
The practical consequence is that the error is about the OS, not about your code - at least until you have proved the port is free. Every fix is therefore one of exactly three moves: release the address (kill the owner), ask for a different address (change the port or bind to a specific interface), or stop asking twice (fix a double listen()). Once you see the error as a tuple collision rather than a framework problem, the whole family becomes legible: EACCES on port 80 is the same bind() call rejected for permissions, and ECONNREFUSED is the mirror image - nothing is listening where you expected something to be.
Stop It From Coming Back
- Read the port from the environment with a fallback -
const port = Number(process.env.PORT) || 3000- so you can sidestep a conflict withPORT=3001 npm startinstead of editing source. - Bind to port 0 in every test that needs a real server, and read
server.address().portfor URLs. The OS hands out a free ephemeral port, so parallel test workers never collide. Withsupertest, pass theappobject and skiplisten()entirely. - Add a SIGINT/SIGTERM handler that calls
server.close()beforeprocess.exit(), so Ctrl-C and container stops release the socket instead of leaving the kernel to clean up. - Put the
listen()call in a single entry file (start.js) and have your app module onlyexportthe app. This makes a double bind structurally impossible when the module is imported by scripts or tests. - In
docker-compose.yml, publish host ports from a variable -ports: ["${API_PORT:-3000}:3000"]- so a developer who already uses 3000 can override it in.envwithout touching the compose file. - Add a
predevnpm script that prints the current owner of your port, e.g."predev": "lsof -i :3000 || true", so a stale listener is visible before the error is.
Related Guides
Errors You Will Probably Hit Next
Error: listen EACCES: permission denied 0.0.0.0:80Error: connect ECONNREFUSED 127.0.0.1:3000Error: listen EADDRNOTAVAIL: address not available 192.168.1.50:3000
EADDRINUSE is a tuple collision, so debug it in that order: prove who owns the address, then decide whether to take the address or ask for a different one. Check the parent of the owning PID before you kill it - a supervisor will just hand the port back to a new process. And if nothing owns the port but the bind still fails, stop looking at the environment and start looking for a second listen() in your own startup path.
댓글
댓글 쓰기