Node.js · beginner

Node.js EADDRINUSE: diagnose an address already in use error

EADDRINUSE is raised by Node.js when the underlying socket layer (libuv → OS) refuses a bind() call because the requested address and port tuple is already owned by another socket, often a stale process from a prior run, a hot-reload worker, or a colliding service on a shared host. The guide walks through identifying the holder, deciding whether to free the port or move to an alternative, and proving the new bind succeeds under the same conditions that originally failed.

The symptoms

  • Node throws Error: listen EADDRINUSE: address already in use :::<port> and exits non-zero on application startup
  • A second app instance (test harness, watcher, or concurrent dev server) refuses to start while the first keeps serving
  • Cluster worker respawn fails repeatedly with EADDRINUSE after the master restarts children, leaving only the master PID bound
  • Container restart loop: orchestrator reports the Node container exits within seconds with EADDRINUSE in the captured stderr
  • Hot-reload tool (nodemon, ts-node-dev, Vite middleware) prints EADDRINUSE after a crash, because the previous process never released the socket

Likely causes

  • A previous Node process did not exit cleanly (unhandled rejection, SIGKILL, debugger detach) and the kernel still lists the socket in TIME_WAIT or as owned by the now-defunct PID
  • Two services are configured for the same port, often a dev server and a preview server both defaulting to 3000
  • SO_REUSEADDR is not set on the listening socket, so the OS refuses a bind to a tuple still in TIME_WAIT after a rapid restart
  • Reverse-proxy or sidecar (nginx, envoy, another language runtime) already binds the port on the same network namespace
  • Systemd, Docker, or a process manager respawns the old binary before the kernel releases the socket, causing an immediate re-bind collision
  • IPv6 dual-stack ambiguity: app binds :: but another listener binds 0.0.0.0, and the kernel treats the wildcards as overlapping for the same port

First ten minutes

  1. 01Capture the exact error line and stack from stderr, including the port number and the host/IP the app tried to bind (e.g., 0.0.0.0:<port> vs :::<port>)
  2. 02List sockets in LISTEN state for the failing port using OS tools, not the app, to identify which PID currently owns the tuple
  3. 03Compare the owning PID against the current Node PID; if they differ, a stale instance is the prime suspect
  4. 04Check process ancestry: a parent process manager that auto-respawns can mask the real culprit behind its own PID
  5. 05Decide between terminating the holder (if it is yours) or reconfiguring the port (if a foreign service owns it) before changing any code
  6. 06If rapid restarts are involved, check whether the socket is in TIME_WAIT rather than LISTEN, which points at SO_REUSEADDR rather than a stuck process

Evidence to collect

  • Full stderr block including Error: listen EADDRINUSE, the syscall reported (bind/listen), the errno, and the requested address tuple
  • Process listing filtered to the Node binary path and to any supervisor binary (pm2, systemd, docker-init) that may mask the owner
  • Output of socket-state enumeration for the port, showing state (LISTEN, TIME_WAIT, CLOSE_WAIT), local address, and owning PID/UID
  • For each candidate owner: start time, command line, and parent PID, to distinguish a fresh process from a stale one
  • If containerized: the container's PID-1 process and whether the orchestrator has a restart policy that re-runs before the socket is released

Where to look

  • Node's net.Server listen() implementation in the official net module, where EADDRINUSE originates from a failed OS bind
  • The OS network namespace shared by the Node process and any sidecar (proxy, debugger agent, metrics exporter) on the same host or pod
  • The kernel socket table for the failing port; a LISTEN entry proves a live owner, a TIME_WAIT entry proves a recent release that blocks re-bind
  • The application's port configuration sources: environment variables, CLI flags, config files, and defaults inside the framework (Express, Fastify, Nest)
  • Process supervisor records: systemd unit ExecStart, pm2 process list, Kubernetes pod spec, Docker compose ports mapping

Diagnostic steps

  1. 01Reproduce the failure with the same command and environment that produced the original error, and confirm the stderr string matches EADDRINUSE exactly
  2. 02Enumerate listeners on the failing port via OS tools (e.g., ss -ltnp 'sport = :<port>' or lsof -nP -iTCP:<port> -sTCP:LISTEN) and record the PID, UID, and local address of every LISTEN entry
  3. 03Enumerate recent releases on the same port via TIME_WAIT entries (e.g., ss -tan state time-wait 'sport = :<port>') to distinguish stuck-owner from rapid-rebind cases
  4. 04Resolve each owning PID to a binary and command line (e.g., ps -o pid,ppid,etime,cmd -p <pid>) to determine whether it is the same Node app, a sibling service, or a supervisor
  5. 05Map the bind address: confirm whether the app binds 0.0.0.0, 127.0.0.1, or ::, and whether the conflicting owner binds a wildcard that overlaps (IPv4 vs IPv6 dual-stack)
  6. 06Inspect supervisor configuration for restart policies that re-exec before TIME_WAIT clears, which would explain a persistent EADDRINUSE across restarts
  7. 07Confirm that no in-process resource still holds the port: child processes, cluster workers, or debug inspector port collisions that the app code may not surface

Common mistakes

  • Blindly killing PIDs on the port without identifying whether the holder is a production service, a sidecar proxy, or a stale dev process; this can cascade into a real outage
  • Changing the app's port to a new value without auditing other config (health checks, reverse proxy upstream, CI scripts), which silently masks the collision instead of resolving it
  • Assuming EADDRINUSE always means a live process; TIME_WAIT blocks bind() for the same tuple and needs SO_REUSEADDR rather than termination
  • Confusing EADDRINUSE with EACCES, which is a permission/IP-binding problem (privileged port or non-local address) and is fixed by a different mechanism
  • Restarting the host or container as a first step; this destroys evidence needed to prove the root cause and can recur if the underlying config is unchanged
  • Ignoring IPv6: binding :: may collide with a process bound to 0.0.0.0 on many kernels, so the fix is not always to switch address families

Safe fixes

  • If the holder is a stale Node process you started earlier and can safely terminate, stop it gracefully (SIGTERM, allow the listen socket to close) before re-launching the app on the same port
  • If rapid restarts produce TIME_WAIT, set server.listen(port, host) with reusePort where supported, or enable SO_REUSEADDR in the net.Server options so the kernel permits re-binding to a tuple in TIME_WAIT
  • If a sidecar legitimately owns the port, move the Node service to a different port and update the reverse proxy upstream and any health-check configuration to match, instead of killing the sidecar
  • If two dev tasks default to the same port, configure each via environment variable (e.g., PORT) and verify the override is read before listen() is called
  • If a supervisor respawns faster than TIME_WAIT clears, lengthen the restart interval or sequence stop-then-start rather than restart-in-place, so the socket fully releases

Prove the fix

  1. 01Restart the app under the exact command and environment that previously produced EADDRINUSE; the process must reach the listening-ready state without throwing and stderr must contain no EADDRINUSE line
  2. 02Re-run the socket enumeration and confirm a single LISTEN entry exists on the requested port, owned by the new PID, with no TIME_WAIT entries from the previous incarnation blocking the bind
  3. 03Trigger a second rapid restart cycle; the second start must also succeed without EADDRINUSE, demonstrating the TIME_WAIT / reuse configuration holds under repeated cycles
  4. 04Issue a service-level readiness check against the new listener (HTTP probe documented generically) and record a 2xx response correlated with the new PID's start time
  5. 05Capture supervisor logs showing exactly one successful start event per restart cycle and zero EADDRINUSE exit events for a defined observation window

Prevention and next steps

  • Make the listen port fully configurable through an environment variable, with a documented default, so deployments never silently collide on a hard-coded value
  • Adopt graceful shutdown in the app: trap SIGTERM/SIGINT, stop accepting new connections, await in-flight requests, then close the server so the OS releases the socket promptly
  • Configure supervisors to stop-then-start on redeploy rather than in-place restart, giving TIME_WAIT time to clear and avoiding EADDRINUSE loops
  • Document a port-allocation table per environment (dev, CI, staging, prod) so engineers can detect a collision before deployment rather than at startup
  • Add a startup probe that fails fast with a clear error when the bind fails, and surface it in logs alongside the owning PID for faster triage next time

Safe commands and checks

ss -ltnp 'sport = :<port>'  # list LISTEN sockets on a port with owning PID/UID; replace <port> with the failing port number
ss -tan state time-wait 'sport = :<port>'  # show TIME_WAIT entries on the port that would block a re-bind without SO_REUSEADDR
lsof -nP -iTCP:<port> -sTCP:LISTEN  # alternative enumeration of LISTEN owners; replace <port> with the failing port number
ps -o pid,ppid,etime,cmd -p <pid>  # resolve an owning PID to its binary, age, and parent; replace <pid> with the PID from the socket listing
pgrep -af 'node'  # list Node processes by command line to find stale instances and their supervisors
kill -TERM <pid>  # graceful stop of a stale owner to release the socket; replace <pid> with the target and verify via the socket listing before relaunching