Intro: What’s the problem and who this is for
When you start a web server, a database, or any networked service, you’ll often see an error like:
Error: address already in use
It means something else is already listening on the same IP + port combination you’re trying to bind.
If you’re a beginner or a hobbyist, the error feels cryptic, and the tools to investigate it ( ss, lsof ) look intimidating.
This tutorial is for Linux users who know a little shell and want a practical, step‑by‑step way to:
- Understand what a port is and why “localhost” matters.
- Locate the offending process with
ssandlsof. - Free the port or change your configuration.
All commands are runnable on any recent distribution (Ubuntu, Fedora, Arch, etc.). No extra packages are required.
Part 1: Core concepts – ports, localhost, and “address already in use”
| Concept | What it means | Typical value |
|---|---|---|
| IP address | Numeric identifier of a network interface. | 127.0.0.1 (loopback), 192.168.1.42 (LAN) |
| Port | 16‑bit number that distinguishes many services on the same IP. | 80 (HTTP), 5432 (PostgreSQL) |
| Socket | Combination of IP + port + protocol (TCP/UDP). | 127.0.0.1:8080/TCP |
| localhost | The loopback interface; traffic never leaves the host. | Always resolves to 127.0.0.1 (IPv4) or ::1 (IPv6) |
| Address already in use | Kernel refuses to bind because another socket already occupies the same IP + port + protocol. | Happens when you start a second copy of a server, or when a previous run didn’t exit cleanly. |
Why “localhost” matters
When you test a service locally, you usually bind to 127.0.0.1 (or 0.0.0.0 to listen on all interfaces). Binding to the loopback address isolates the service from the outside world, which is great for development. However, the same “address already in use” rule applies: only one process can bind to 127.0.0.1:PORT/TCP at a time.
Part 2: Using ss to list listening sockets
ss (socket statistics) ships with the iproute2 package and is faster than the older netstat. The most useful invocations are:
# Show all listening TCP sockets (IPv4 + IPv6)
ss -tlnp
Explanation of flags:
| Flag | Meaning |
|---|---|
-t |
TCP only |
-l |
Listening sockets |
-n |
Show numeric addresses (no DNS look‑ups) |
-p |
Show the PID / program name that owns the socket |
Sample output:
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 127.0.0.1:5432 *:* users:(("postgres",pid=1245,fd=5))
LISTEN 0 511 0.0.0.0:22 *:* users:(("sshd",pid=743,fd=3))
To focus on a single port, add a filter:
ss -tlnp 'sport = :8080'
If the output shows a process, you’ve found the culprit.
Part 3: Using lsof as an alternative
lsof (“list open files”) can also reveal which process has a network socket open. It’s especially handy when you need to see both TCP and UDP or when you want to grep for a program name.
# List all network sockets that are listening
sudo lsof -i -P -n | grep LISTEN
Flags:
| Flag | Meaning |
|---|---|
-i |
Show network files |
-P |
Show ports as numbers (no service name lookup) |
-n |
Skip host name resolution |
sudo |
Required because non‑root users can’t see other users’ sockets |
To look for a specific port:
sudo lsof -iTCP:8080 -sTCP:LISTEN
Typical output:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
python3 3172 alice 4u IPv4 12345 0t0 TCP 127.0.0.1:8080 (LISTEN)
Now you know exactly which PID is holding the port.
Hands‑on example: Freeing port 8080 for a simple Flask app
We’ll create a tiny Python Flask server that listens on 127.0.0.1:8080. Then we’ll simulate a conflict, locate the offender, and resolve it.
1. Install Flask (if you don’t have it)
python3 -m venv venv
source venv/bin/activate
pip install Flask
2. Minimal Flask app (app.py)
# app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello from Flask!"
if __name__ == "__main__":
# Bind explicitly to localhost:8080
app.run(host="127.0.0.1", port=8080)
3. Run it in the background
nohup python3 app.py > flask.log 2>&1 &
echo $! # prints the PID, e.g. 4521
You can test it:
curl http://127.0.0.1:8080/
# → Hello from Flask!
4. Simulate a conflict – start a second server on the same port
Open a new terminal and run:
python3 -c "import socket, time; s=socket.socket(); s.bind(('127.0.0.1',8080)); s.listen(); time.sleep(60)"
You’ll see:
OSError: [Errno 98] Address already in use
5. Find the offending process with ss
ss -tlnp 'sport = :8080'
Output (example):
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 127.0.0.1:8080 *:* users:(("python3",pid=4521,fd=3))
The PID is 4521 – that’s our Flask app.
6. Kill the process (or change its port)
kill 4521 # graceful SIGTERM
# If it doesn’t exit:
kill -9 4521 # forceful SIGKILL
Verify the port is free:
ss -tlnp 'sport = :8080' # should return nothing
7. Restart the second server (the one we wrote with socket)
python3 -c "import socket, time; s=socket.socket(); s.bind(('127.0.0.1',8080)); s.listen(); print('Listening...'); time.sleep(60)"
Now the socket command works because the Flask process is gone.
Common mistakes / troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
ss -tlnp shows no PID column (empty) |
You ran the command without sudo. Non‑root users can’t see other users’ sockets. |
Prefix with sudo. |
lsof returns Permission denied |
Same as above – you need root privileges to see all sockets. | Run sudo lsof …. |
| Killing the PID doesn’t free the port | The process has multiple threads each holding a socket, or it spawned a child that inherited the socket. | Use pkill -f <program> or check ps -ef | grep <pid> for children, then kill them. |
Port still appears busy after kill |
The process is in TIME_WAIT state (common for TCP). | Wait a few seconds, or enable SO_REUSEADDR in your code (advanced). |
ss shows 0.0.0.0:PORT but you expected 127.0.0.1:PORT |
The service bound to all interfaces, not just localhost. | Adjust the program’s bind address (e.g., host="127.0.0.1"). |
Try it yourself
- Create a conflict: Run any simple TCP server (Python, Netcat, etc.) on port 9090.
- Locate it: Use
ss -tlnp 'sport = :9090'andsudo lsof -iTCP:9090 -sTCP:LISTEN. - Free it: Kill the PID, then verify the port is empty.
If you can repeat the steps without errors, you’ve mastered the basics.
What’s next
- Understanding
SO_REUSEADDRandSO_REUSEPORT– how to design services that can restart without “address already in use”. - Using
iptablesorfirewalldto block external access while keeping localhost open. - Automating port cleanup with a small Bash helper script.
- Diagnosing UDP port conflicts – similar tools, slightly different flags.
Stay tuned for the follow‑up post that dives into those topics!
Bookmark this page for the next time “address already in use” pops up.