Fix "Address Already in Use" on Linux (Port Conflicts)
Track down what owns a busy port, decide whether to kill it or rebind, and stop TIME_WAIT from blocking your restart.
Key takeaways
- Another process already holds that port.
- Find it with sudo ss -ltnp 'sport = :8080' or sudo lsof -i :8080, then kill the stale PID or start your server on a different port.
- If nothing is listening and bind still fails, a TIME_WAIT socket is blocking it: set SO_REUSEADDR before bind().
On this page
"Address already in use" means another socket already owns that port. Find the owner with sudo ss -ltnp 'sport = :8080' (or sudo lsof -i :8080), then either kill the stale PID or start your server on another port. If nothing is listening and bind() still fails, a TIME_WAIT socket is in the way: set SO_REUSEADDR before binding.
$ ./myserver
error: listen tcp 0.0.0.0:8080: bind: address already in use
Node calls this EADDRINUSE, Python raises OSError: [Errno 98], Go prints the line above, and uvicorn reports [Errno 98] error while attempting to bind on address. Same kernel error underneath: bind(2) returned EADDRINUSE.
What the error actually means
A listening socket is a tuple: protocol, local address, local port. The kernel lets exactly one socket own a given host:port combination for a protocol at a time. When your process asks to bind() to 0.0.0.0:8080 and something already holds that tuple, the kernel refuses. It is not a firewall, not DNS, not permissions (usually). Something is already listening, or a recently closed socket is still holding the port.
Two questions get you to a fix: what owns the port, and do you want that thing gone or do you want a different port.
Find the culprit
ss is the modern tool. -l listening, -t TCP, -n numeric, -p show the process:
$ sudo ss -ltnp 'sport = :8080'
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 511 0.0.0.0:8080 0.0.0.0:* users:(("node",pid=4821,fd=23))
PID 4821, an old node process, still holds 8080. Run ss with sudo, otherwise the Process column comes back empty for sockets you do not own.
lsof gives the same answer plus the user, and fuser is the quickest when you just want the PID:
$ sudo lsof -nP -i :8080
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node 4821 kiril 23u IPv4 512377 0t0 TCP *:8080 (LISTEN)
$ sudo fuser 8080/tcp
8080/tcp: 4821
On Windows the equivalent is netstat -ano | findstr :8080, then taskkill /PID 4821 /F.
Kill it or move
If the process is a stale copy of your own service, kill it:
$ sudo kill 4821 # SIGTERM, lets it clean up
$ sudo kill -9 4821 # SIGKILL, only if it ignores the first
$ sudo kill $(sudo lsof -t -i :8080) # lookup and kill in one line
fuser -k 8080/tcp does the same. Be deliberate here: on a shared box that PID might be someone else's database. Confirm the command name before you signal anything.
If the port legitimately belongs to another service, do not fight it. Change your own port: PORT=8081 ./myserver, or uvicorn main:app --port 8001.
The TIME_WAIT case: your just-stopped server can't rebind
You stop your server, immediately restart it, and get address already in use even though ss -ltn shows nothing listening. What you will see instead is a socket in TIME_WAIT:
$ ss -tan | grep 8080
TIME-WAIT 0 0 10.0.1.5:8080 10.0.1.9:52344
When a TCP connection closes, the side that closed first keeps the socket in TIME_WAIT for twice the maximum segment lifetime, a fixed 60 seconds on Linux. That lets straggling packets from the old connection die out. During that window the kernel, by default, refuses to bind a fresh listening socket to the same address and port.
The wrong fix is to wait, or to tune tcp_fin_timeout (which controls FIN_WAIT_2, not TIME_WAIT). The right fix lives in your code: set SO_REUSEADDR on the listening socket before you bind. Per socket(7), it lets a socket bind "except when there is an active listening socket bound to the address".
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # before bind()
s.bind(("0.0.0.0", 8080))
s.listen()
Most frameworks set this for you. Python's http.server (allow_reuse_address = True), Go's net.Listen, Node's net, and asyncio-based servers like uvicorn all enable it, which is why you rarely hit TIME_WAIT in those. If you wrote the socket code by hand and skipped it, this is your bug.
SO_REUSEPORT is different: it lets multiple sockets bind the same port at once so several workers can each accept connections, with the kernel load-balancing across them. All of them must set it and share the same effective UID. Reach for it when you want that behavior, not as a TIME_WAIT workaround.
0.0.0.0 vs 127.0.0.1 conflicts
0.0.0.0 means every interface on this host; 127.0.0.1 means loopback only. A socket bound to 0.0.0.0:8080 covers 127.0.0.1:8080 too, so a second process trying 127.0.0.1:8080 collides with it. When ss shows 0.0.0.0:8080 and you thought you only bound loopback, that wildcard bind is the thing in your way.
Privileged ports (below 1024)
Binding a port under 1024 as a normal user fails with bind: permission denied. That is EACCES, not EADDRINUSE, but it lands in the same "won't start on this port" bucket. Rather than run the whole service as root, grant just the bind capability:
$ sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/myserver
For interpreted apps, listen high (8080) and let a reverse proxy handle 80.
The Docker cousin: "port is already allocated"
docker run -p 8080:80 nginx can fail with Bind for 0.0.0.0:8080 failed: port is already allocated, and Compose prints failed to bind host port 0.0.0.0:8080/tcp: address already in use. Same root cause, one layer up: something on the host, or another container's published port, already holds 8080. Use ss -ltnp on the host plus docker ps to spot the container. Full walkthrough: Docker port already allocated.
Questions people ask
How do I fix "address already in use" with nc?
nc -l 8080 fails when another listener, often a previous nc you backgrounded or left in another terminal, still holds the port. Run sudo ss -ltnp 'sport = :8080', kill the PID it prints, and retry. If the port must stay with its current owner, listen on a free one instead. Netcat cannot share a port that has an active listener.
Why does uvicorn say address already in use?
Uvicorn binds 127.0.0.1:8000 by default, so the usual cause is a second uvicorn still running: a forgotten terminal, a crashed --reload session that left a worker behind, or a container publishing 8000. Find it with sudo lsof -i :8000, kill that PID, or start on another port with uvicorn main:app --port 8001.
How do I kill whatever is using a port?
sudo kill $(sudo lsof -t -i :8080) sends SIGTERM to every process holding 8080; sudo fuser -k 8080/tcp does the same in one command. Check the process name with ss -ltnp first, because on a shared host that PID might be a database or another team's service. Use kill -9 only if SIGTERM is ignored.
The call we'd make
Run sudo ss -ltnp 'sport = :PORT' first, every time. If the owner is a stale copy of your own service, kill it and restart. If it belongs to something real, move your service to another port instead of fighting for it. If nothing is listening but you still can't bind, fix TIME_WAIT with SO_REUSEADDR in the code rather than sleeping through the timer. Ninety percent of these are a forgotten background process, and ss finds it in seconds. For the broader toolkit, see our Linux troubleshooting guide.
Sources
Best LLM APIs and AI Infrastructure in 2026 — A Cost and Capability Map
The LLM stack is a maze of APIs, GPU clouds, gateways, and serving tools. This is the map to what each layer is for and how to keep the bill sane.
CircleCI Best Practices in 2026: Fast, Safe Pipelines
A production-focused CircleCI guide: orbs, reusable commands and executors, workflows with fan-in/fan-out, contexts and OIDC for secrets, layered caching, test splitting, approval jobs, and dynamic config — with copy-paste examples.
More from Linux
Explore more articles in this category
CERN Left Red Hat for Debian: The CPU Baseline Lesson
CERN is moving accelerator control computers to Debian because RHEL raised its minimum CPU level. Check your own fleet's x86-64-vN support before the next OS upgrade does it for you.
ext4 vs XFS vs Btrfs: Choosing a Filesystem for a Server
The default filesystem your distro picks is not always the right one for your workload. Here is what actually differs and when each one wins.
journald Log Management: Retention, Filtering, and Forwarding
journald is the default log sink on every systemd distro, and most of it runs on defaults nobody chose. Here is how to actually control it.
You might have missed
Evergreen posts worth revisiting.