Fixing "Address already in use" - Port Conflicts

Fixing "Address already in use" - Port Conflicts

What You'll Learn

  • Why Address already in use (EADDRINUSE) appears
  • How to identify the process holding the port and release it safely
  • How to isolate the TIME_WAIT case where the error returns "even though I just stopped it"

Quick Summary

  1. Find who holds it: sudo ss -lntp | grep ':PORT ' (or sudo lsof -i :PORT)
  2. Stop it correctly: systemctl stop for services, kill PID for one-off processes
  3. Error but no process: it's TIME_WAIT — set SO_REUSEADDR in the app or wait ~60s

Prerequisites

  • OS: Ubuntu (systemd environment)
  • Target: Anyone who hit Address already in use starting their own server/app
  • Examples use port 8080 (substitute your own)

What Is "Address already in use"?

Conclusion: The port your app tries to reserve with bind() is already held by another process, so the OS refuses the reservation.

A server process declares "listen on this port" at startup via the bind() system call. The same IP/port pair cannot be reserved twice, so if it is already taken, bind() returns EADDRINUSE.

Typical messages:

Error: listen EADDRINUSE: address already in use :::8080
bind: Address already in use
OSError: [Errno 98] Address already in use

Errno 98 is the Linux number for EADDRINUSE. The cause is identical across languages and frameworks.

Why Does the Port Conflict?

Conclusion: Almost every case is one of three: an old process still alive, a double start, or TIME_WAIT from a just-killed process.

  • Old process still running: you thought you restarted, but the old one survived. Easy to miss with detached starts (& / nohup / containers)
  • Double start: the same app launched twice, or another app uses the same port
  • TIME_WAIT: connections from the just-terminated process linger in TCP TIME_WAIT, and an app without SO_REUSEADDR fails to rebind

Cases 1 and 2 are solved by stopping the process. Only case 3 has no process to stop and needs a different approach (below).

How Do I Find Which Process Uses the Port?

Conclusion: ss -lntp is the fastest way to get the listener and its PID. lsof / fuser show the same data differently.

sudo ss -lntp | grep ':8080 '
LISTEN 0 511 0.0.0.0:8080 0.0.0.0:* users:(("node",pid=12345,fd=18))
  • users:(("node",pid=12345,...))pid=12345 is the owner
  • -l listening / -n numeric / -t TCP / -p process (needs sudo)

Confirm with lsof

sudo lsof -i :8080
COMMAND   PID  USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
node    12345  hide   18u  IPv4  98765      0t0  TCP *:8080 (LISTEN)

Get just the PID with fuser

sudo fuser 8080/tcp
8080/tcp:            12345

ss also shows whether the bind is 127.0.0.1:8080 or 0.0.0.0:8080. The same port number on 127.0.0.1 versus 0.0.0.0 is treated separately, which helps judge whether a real conflict exists.

How Do I Stop the Process and Release the Port?

Conclusion: Use systemctl stop for managed services and kill PID for manual processes. kill -9 is a last resort.

When it runs as a service

sudo systemctl stop myapp.service

If you kill a systemd-managed process directly, an auto-restart (Restart= setting) may revive it and grab the port again. Always use systemctl stop.

When it is a manual process

Target the PID you found, trying the normal signal (SIGTERM) first:

sudo kill 12345

Only if it survives after a few seconds, force-kill it (SIGKILL):

sudo kill -9 12345

kill -9 gives the process no chance to clean up (close connections, remove temp files) and kills it instantly. It can cause data corruption or leftover lock files, so try kill (SIGTERM) first and use -9 only as a last resort.

Then confirm the port is free:

sudo ss -lntp | grep ':8080 '

No output means it is released.

Error but No Process (TIME_WAIT)?

Conclusion: Connections from the just-stopped process linger in TIME_WAIT. With SO_REUSEADDR the app can restart instantly; without it, the state clears itself in ~60s.

If ss -lntp finds no listener yet Address already in use appears, suspect TIME_WAIT. Check:

ss -tan state time-wait | grep ':8080'

TIME_WAIT is a normal TCP state that prevents stray packets from being misread, and it usually clears in about 60 seconds. Options:

  • Enable SO_REUSEADDR in the app (recommended, permanent fix): most server implementations set this by default; a custom server should set it on the listen socket
  • Wait ~60s and restart: a stopgap

How Do I Prevent Recurrence?

Conclusion: Implement graceful shutdown and SO_REUSEADDR in the app, and make the startup script reliably stop the old process.

  • Set SO_REUSEADDR: avoids restart failures caused by TIME_WAIT
  • Graceful shutdown: on SIGTERM, close connections before exiting; reduces reliance on kill -9
  • Delegate process management to systemd / containers: manual & starts easily leave old processes behind
  • Pre-start check: at the top of the startup script, run ss -lntp | grep ':PORT ' and stop any holder before launching

Things to Avoid

Conclusion: "Jump straight to kill -9", "dodge by changing the port", and "enable tcp_tw_recycle" all breed recurrence and new failures.

Don't: Spam kill -9 without finding the cause

If the holder is a systemd-managed service, kill only triggers a restart. Identify it with ss first, then systemctl stop.

Don't: Dodge by Changing the Port Number

Switching 8080 to 8081 every time it conflicts destroys your sense of "what runs on which port". Identify and release the holder instead.

Don't: Enable tcp_tw_recycle

It lingers in old blog posts, but it breaks connectivity behind NAT and has been removed from current kernels. Use SO_REUSEADDR on the listen side.

Copy-Paste: From Identify to Release

# 1. Identify the holder
sudo ss -lntp | grep ':8080 '
sudo lsof -i :8080

# 2-a. If it is a service
sudo systemctl stop <service>

# 2-b. If it is a manual process (SIGTERM -> last resort SIGKILL)
sudo kill <PID>
sudo kill -9 <PID>

# 3. Confirm release
sudo ss -lntp | grep ':8080 '

# Error but no process (TIME_WAIT)
ss -tan state time-wait | grep ':8080'

Next Reading