/개발/Fixing EADDRINUSE and BindException Port Conflicts: OS-Specific PID Kill Commands
DevelopmentEADDRINUSE포트충돌 해결

Fixing EADDRINUSE and BindException Port Conflicts: OS-Specific PID Kill Commands

Diagnose EADDRINUSE, java.net.BindException, and nginx bind failures by cause, with copy-paste commands to find and kill port-occupying processes on Linux, macOS, and Windows, plus SO_REUSEADDR and TIME_WAIT techniques to prevent recurrence

Fixing EADDRINUSE and BindException Port Conflicts: OS-Specific PID Kill Commands

Conquering address already in use: Fixing EADDRINUSE and BindException Port Conflicts

You hit deploy and the service never comes up—just one red log line. Node yells EADDRINUSE, Java yells java.net.BindException, nginx yells bind() failed. The messages look different, but they all mean the same thing: someone is already holding the port you want. This post is a no-panic playbook: branch the cause into three cases and recover in five minutes with copy-paste commands for each OS.

Same collision, different error messages

Here are the errors the three runtimes spit out.

TEXT
# Node.js
Error: listen EADDRINUSE: address already in use :::3000

# Java
java.net.BindException: Address already in use

# nginx
bind() to 0.0.0.0:80 failed (98: Address already in use)

At the OS level, they all mean the bind() syscall returned EADDRINUSE(errno 98). In other words, the diagnosis procedure is the same no matter which language your server is written in.

Cause-branching table by error message

Error messageMost common causeSecond thing to suspect
EADDRINUSE :::3000 (Node)Previous dev server/nodemon still aliveDuplicate start caused by hot reload
java.net.BindException (Java)Duplicate run of the same JAR/IDE instanceTIME_WAIT leftover on restart + SO_REUSEADDR not set
bind() to 0.0.0.0:80 failed (98) (nginx)nginx/Apache already running and occupying 80Ports 80/443 require root (don't confuse this with a permission issue)

Three-way diagnosis decision tree

Before you recover, figure out which case you're in.

  1. An already-running process is occupying the port → Most common. ss/lsof finds a PID. → Kill that process.
  2. TIME_WAIT leftover from a closed socket → You just stopped the server and started it again immediately. No PID, but the port is blocked. → SO_REUSEADDR or wait a bit.
  3. Restart fails because SO_REUSEADDR is not set → Companion of #2. If it keeps happening in your restart script, fix it in code/config.

How to tell them apart: if ss -ltnp shows a LISTEN-state PID, it's case 1. If nothing shows but ss -tan state time-wait has that port piled up, it's case 2 or 3.

Find and kill the occupying PID by OS (copy-paste ready)

Linux

These days ss is the standard over netstat (plenty of servers don't have net-tools installed).

Bash
# 1. Check LISTEN process + PID (recommended)
sudo ss -ltnp | grep :8080

# 2. Check with lsof
sudo lsof -i :8080

# 3. netstat (legacy environments)
sudo netstat -tlnp | grep 8080

# 4. Kill the port-occupying process in one shot
sudo fuser -k 8080/tcp

macOS

macOS has neither ss nor fuser. Find the process with lsof and kill it yourself.

Bash
# Check occupying PID (-n: skip DNS, -P: show port numbers as-is)
lsof -nP -i :8080

# Kill
kill -9 <PID>

Windows

CMD
:: Check occupying PID (rightmost column is PID)
netstat -ano | findstr :8080

:: Force kill
taskkill /PID <PID> /F

Root-cause fixes and preventing recurrence

Handling leftover TIME_WAIT

Collisions from stopping the server and starting it again immediately are almost always fixed by SO_REUSEADDR.

JavaScript
// Node.js — usually the default, but you can make it explicit via the options object
const server = require('http').createServer(app);
server.listen({ port: 3000, host: '0.0.0.0' });
JAVA
// Java raw socket
ServerSocket socket = new ServerSocket();
socket.setReuseAddress(true);   // must be called before bind
socket.bind(new InetSocketAddress(8080));

Spring Boot's embedded Tomcat enables SO_REUSEADDR by default, so if collisions keep happening, first check that the port in application.yml isn't overlapping with another instance.

Bash
# Check leftover TIME_WAIT count
ss -tan state time-wait | wc -l
Bash
# Kernel tuning (note: tcp_tw_reuse applies only on the client/outbound side)
sudo sysctl -w net.ipv4.tcp_tw_reuse=1
# Shorten wait time after FIN
sudo sysctl -w net.ipv4.tcp_fin_timeout=30

One common misconception. tcp_tw_reuse does not clear TIME_WAIT on a LISTEN server port. The right answer for server-side restart collisions is SO_REUSEADDR, not kernel tuning. I've seen plenty of people turn on tcp_tw_reuse in production, see no effect, and waste time.

When the previous instance doesn't die on a systemd restart

If you have Restart=always but the main process dies leaving children behind, those children keep the port and the new instance collides trying to start. Set KillMode=mixed so the whole process group is cleaned up.

INI
[Service]
ExecStart=/usr/bin/node /app/server.js
ExecStop=/bin/kill -SIGTERM $MAINPID
Restart=always
KillMode=mixed        # SIGTERM to main, SIGKILL to remaining children
TimeoutStopSec=10

[Install]
WantedBy=multi-user.target
Bash
# Check shutdown failure via status and logs
systemctl status myapp
journalctl -u myapp -n 50

Zombie and orphan processes, and containers

Bash
# Trace the app and parent PID (PPID)
ps -ef | grep myapp

A Z state (zombie) will not die from kill -9. Zombies disappear only when the parent reaps them with wait(), so you need to terminate the parent process. If you run the app directly as PID 1 in a container, zombies pile up because nothing reaps them—use the --init option or tini. In Kubernetes, Pods with overlapping hostPort on the same node can also collide, so check whether you're using a Service instead of hostPort.

Wrap-up: checklist when a collision happens

  1. ss -ltnp | grep :PORT — is there a LISTEN PID?
  2. If yes → kill the process (fuser -k, kill, taskkill)
  3. If no PID but still blocked → check TIME_WAIT (ss -tan state time-wait)
  4. If it repeats on every restart → set SO_REUSEADDR / systemd KillMode=mixed
  5. If the dev server won't die in CI/hot-reload → clean up the process group in the shutdown hook

FAQ

Q. I killed it with kill -9 but the port is still blocked. A. If no PID shows up, it's likely leftover TIME_WAIT rather than a process collision. Check with ss -tan state time-wait, and if it's a restart collision, enable SO_REUSEADDR. It usually clears itself within a few tens of seconds.

Q. I get ss: command not found on macOS. A. macOS has neither ss nor fuser. Find the PID with lsof -nP -i :PORT, then kill it with kill -9 <PID>.

Q. Bind fails on ports 80/443 but nothing is occupying them. A. Ports below 1024 require root. It may be a permission issue—run with sudo, or bind to an unprivileged port and put a reverse proxy in front.

확인 정보
✦ ✦ ✦
편집 검토 · Editorial Review

Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.

편집 책임 · Nodelog 기술 편집팀·발행 · ·업데이트 ·

Comments

Be the first to comment.