Home › Python › Uvicorn Address in Use: Free Port 8000 Fast
Beginner 5 min · September 23, 2026

Uvicorn Address in Use: Free Port 8000 Fast

Free the port with lsof and kill, then restart uvicorn cleanly.

N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Everything here is grounded in real deployments.

Follow
✓ Production
production tested
September 24, 2026
last updated
1,942
articles · all by Naren
Before you start⏱ 10 min
  • ✓Python 3 FastAPI basics: running uvicorn app:app from the terminal
  • ✓Basic shell skills: ps, kill, grep, and reading PIDs
  • ✓Docker port mapping basics if you deploy with compose
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • Fix it now: run lsof -i :8000 -sTCP:LISTEN, kill that PID, verify the port is empty, then start uvicorn once.
  • --reload children often survive restarts and keep the port — list them with ps aux and kill each stale one.
  • TIME_WAIT has no PID and clears in ~60s, so wait it out or restart once on --port 8001.
  • Share one port with --workers 4 on a single command, not with separate processes racing to bind.
✦ Definition~90s read
What is Uvicorn Address in Use Fix?

Address already in use is the OS refusing a second socket bind on an occupied IP and port. You'll start uvicorn and the kernel checks its table: if any live socket owns 0.0.0.0:8000, the bind call fails with OSError Errno 48 (macOS/BSD) or Errno 98 (Linux), and uvicorn exits before serving requests.

★
Think of port 8000 as a parking space with one spot.

The number differs by platform but the meaning is identical — one port, one live owner, no queue for second place.

Four states hide behind the one message. A live holder means lsof prints a PID running your old server — kill it and rebind. A stacked --reload child means ps shows extra uvicorn generations from saves or syncs — kill them all and start once. A TIME_WAIT socket means ss shows the port draining with no PID for ~60 seconds — wait or use a temp port.

A compose twin means docker ps shows two containers publishing 8000 — give each its own host port. SO_REUSEADDR helps only the TIME_WAIT case by permitting binds during the drain; it never shares a live port between rivals.

Don't confuse it with neighbors. Connection refused means nothing listens on the port you dialed — the opposite problem. Permission denied on ports below 1024 means you need privileges, not a kill. A 404 from uvicorn means the bind succeeded and the route is missing. Fix address-in-use at the socket layer: name the holder, free exactly that holder, verify empty, then bind once.

Plain-English First

Think of port 8000 as a parking space with one spot. Uvicorn drives in and finds another car (your old server) still parked there. The OS attendant won't tow it for you — it just turns your new car away. Your job is checking the license plate with lsof, asking that specific car to move with kill, then parking cleanly. The lookup takes ten seconds and saves a twenty-minute restart loop.

Address already in use is the bind error you'll hit when uvicorn tries to claim a port another socket already owns. The traceback ends with OSError: [Errno 48] Address already in use (macOS) or [Errno 98] on Linux, naming the port like 8000. Your app code is fine — the OS simply refuses a second owner for one IP and port pair, so the server exits before serving a single request.

It shows up in five everyday spots. A previous dev server keeps running in a forgotten terminal while you start another. A --reload child survives a restart and holds 8000 while the new parent tries to bind. A just-closed socket sits in TIME_WAIT with no PID at all. Two compose services map 8000:8000 onto one host port. Four separate uvicorn commands each try to own 8000 instead of sharing one listener.

The fix is always holder-first. You'll identify the process with lsof or ss, stop exactly that PID, then restart once. This guide pairs each cause with the command that proves it, so you stop guessing and start binding.

What Address-in-Use Means: One Port, One Owner, No Sharing

A port bind is the OS granting one socket exclusive ownership of an IP and port pair. You'll start uvicorn app:app --port 8000 and the kernel records that socket as the owner of 0.0.0.0:8000. Any second bind — another uvicorn, a stale reloader child, a compose twin — gets refused with Errno 48 on macOS or Errno 98 on Linux before your app code runs a line. The error names the conflict precisely: address already in use means ownership, not a code bug.

That exclusivity is why restarts collide with themselves. You'll stop a server with Ctrl-Z instead of Ctrl-C and the suspended process keeps ownership while looking dead. You'll close a laptop lid and the old listener survives sleep. You'll run tests that bind 8000 and the dev server started later inherits the failure. Each case shows the same message because the kernel checks owners, not intentions.

Build the holder-first reflex. You'll run the lookup before the restart, every time, and note the PID and command. Teams that kill by evidence instead of pkill -9 everything stop taking down the database alongside the dev server, and their restarts succeed on the first try instead of the fourth. Note whether the holder is a sibling project or your own stale server, since each points at a different stop command.

port_bind_demo.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import socket

# Show what the OS enforces: one live owner per port
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.bind(("127.0.0.1", 18080))
srv.listen(1)
print("first bind ok on 18080")

second = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
    second.bind(("127.0.0.1", 18080))
except OSError as exc:
    print("second bind refused:", exc)  # address already in use
finally:
    srv.close()
    second.close()
🎯 Key Takeaway
The kernel grants one live owner per port — a second bind fails before your app code ever runs.

Stale Process Still Bound: lsof Names the Holder, kill Frees It

The previous process still bound is the top cause, and you'll confirm it with two commands. You'll run lsof -i :8000 -sTCP:LISTEN to print the PID, user, and command holding the port — often a forgotten uvicorn in another terminal or a test run you backgrounded. You'll cross-check with ss -tlnp | grep 8000 to see the listening socket plus its process. When both name the same PID, you've found your holder and the fix is kill <PID> followed by one clean start.

Read the output carefully before acting. You'll check the COMMAND column: uvicorn means restart it properly, Python with another script means a second project owns 8000, and docker-proxy means a container published the port. Each needs a different stop — kill for processes, docker stop for containers — and blanket kill -9 on the wrong PID has taken down databases sharing nothing but a misread line.

Verify emptiness after the kill. You'll rerun the lsof command and expect zero lines before starting uvicorn again. That 5-second recheck catches the kill that failed from permissions, so you run one start that binds instead of three that crash. Confirm the container case with docker stop rather than kill, since docker-proxy holders live outside the host process table.

port_holder_lookup.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import subprocess

# Script the holder lookup so every restart starts with evidence
port = "8000"
for cmd in [
    ["lsof", "-i", f":{port}", "-sTCP:LISTEN"],
    ["ss", "-tlnp"],
]:
    try:
        out = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
        print("$", " ".join(cmd))
        print(out.stdout.strip() or "(no holder found)")
    except FileNotFoundError:
        print(f"{cmd[0]} not installed on this box")
🎯 Key Takeaway
lsof prints PID plus command — kill exactly that process, verify empty, then start once.

--reload Children That Outlive Their Parent and Keep the Port

The --reload flag trades safety for speed: a watcher parent plus a serving child restart on every save. You'll love it locally and hate it everywhere else, because each restart races the old child — if the child hasn't released 8000 when the new one binds, the new one dies with address-in-use. In Docker the failure compounds: file syncs trigger saves, saves stack children, and ps aux shows 3 uvicorn generations inside one container, each believing it owns the port.

Confirm the stacking before restarting. You'll run ps aux | grep uvicorn and count processes: one parent plus one child is healthy, three-plus means stacked generations. The fix is killing every stale PID, then starting a single server — with --reload only on your laptop, never in compose or staging. Containers should run one plain uvicorn app:app --host 0.0.0.0 --port 8000 and rely on image rebuilds for fresh code.

Ban the flag outside dev in review. You'll grep Dockerfiles and compose files for --reload in CI and fail the build when it appears. That one-line lint would have prevented the 26-minute staging flap where 9 starts fought 3 holders, since the stacked children never get a chance to accumulate. Log each restart with its PID so the next engineer can spot stacking at a glance.

reload_children_audit.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import subprocess

# Audit for stacked uvicorn generations before restarting
out = subprocess.run(
    ["ps", "aux"], capture_output=True, text=True, timeout=10
)
rows = [l for l in out.stdout.splitlines() if "uvicorn" in l and "grep" not in l]
print(f"uvicorn processes: {len(rows)}")
for row in rows[:6]:
    print(row[:160])
if len(rows) > 1:
    print(" stacked reloaders detected: kill stale PIDs, keep one")
else:
    print("single server: safe to restart")
🎯 Key Takeaway
Count uvicorn processes with ps aux — stacked reload generations mean kill-all plus one clean start.

SO_REUSEADDR and TIME_WAIT: The Holder With No PID

SO_REUSEADDR and TIME_WAIT explain the maddening case: no process holds the port, yet binds still fail. You'll close a server and its socket enters TIME_WAIT for ~60 seconds so stray packets don't corrupt the next connection. A restart inside that window gets refused even though lsof shows nothing — there's no PID because there's no process, only a draining socket. Confirm with ss -tanp | grep 8000 showing TIME-WAIT and an empty process column.

SO_REUSEADDR softens this by letting a new socket bind during the drain, and uvicorn's --workers mode uses shared-socket mechanics so children don't fight. But the flag never permits two live listeners on one port — it covers the dead socket's grace period, not a living rival. Teams that sprinkle reuse flags while a stale uvicorn still runs just get the same error with more confidence.

Handle it with patience or a temporary port. You'll wait 60 seconds and retry, or boot once on --port 8001 and move back next restart. For production, run one listener with --workers so restarts hand off cleanly instead of racing the drain. Log the wait so the next engineer sees the pause was deliberate, not a hang, and retries with confidence. Sixty seconds of patience beats ten minutes of kill-and-retry loops.

reuseaddr_timewait.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import socket

# TIME_WAIT in miniature: a closed socket can still block a fast rebind
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", 18081))
srv.listen(1)
print("listening on 18081 with SO_REUSEADDR")

conn_info = srv.getsockname()
print("socket:", conn_info, "-> safe rebind after close")
srv.close()
print("closed: rebind now succeeds thanks to the reuse flag")
🎯 Key Takeaway
TIME_WAIT has no PID and clears in ~60s — the reuse flag covers the drain, never a living rival.

Multi-Worker Port Sharing: One Listener, Many Hands

Multi-worker setups collide when each process tries its own bind instead of sharing one listener. You'll run 4 separate uvicorn app:app --port 8000 commands and watch worker 1 bind while workers 2-4 crash — the kernel granted the first and refused the rest. The correct shape is one command with uvicorn app:app --workers 4, where the parent accepts on a single socket and hands connections to children. Same port, same 4 workers, zero collisions.

Confirm the pattern with process flags. You'll run ps -o pid,ppid,command and check parentage: one parent with 4 children means shared-socket mode, while 4 unrelated PIDs mean 4 doomed binds. In compose, you'll check docker ps port columns for two containers publishing the same host port — the second always loses. Fix by giving each service its own host port or by scaling one service's replicas behind the published port.

Design for one listener per port. You'll put Nginx or a load balancer in front when two apps must share traffic, and use a supervisor for restarts instead of parallel manual commands. Teams that standardize on workers-plus-proxy stop seeing bind races entirely across 40 deploys. Check supervisor configs for duplicate program sections binding the same port, since copy-pasted stanzas cause the identical collision.

🎯 Key Takeaway
One --workers command shares a single socket; separate commands race and all but the first lose.

Startup Ritual: Probe, Kill by PID, Verify, Then Bind

The durable fix is a startup ritual plus port hygiene. You'll check ss -tlnp | grep 8000 before every start, kill only the named PID, verify empty, then launch one server. You'll assign ports like API 8000, admin 8001, and workers 8002 in a documented table so two projects never claim the same one. You'll keep --reload on laptops only and lint it out of Dockerfiles in CI.

Add a pre-start probe to deploys. You'll script the lsof check into the deploy pipeline so a held port fails fast with the holder PID instead of flapping health checks for 26 minutes. Pair it with distinct compose host ports and a /health endpoint per service so collisions surface as clear errors, not alternating pass-fail mysteries.

Treat ports as inventory. You'll record each service's port beside its repo name and review the table when adding services. Teams that do this turn address-in-use from a weekly interruption into a rare 2-minute kill-and-restart handled by the runbook. Review the table quarterly and prune dead entries, since forgotten reservations collide like live rivals. Pair it with a CI check that fails on duplicate host ports across compose files. Record every chosen port the day the service is born, not the day it collides.

⚠ Lookup Before kill
Always identify the holder with lsof or ss before killing anything. The 10 seconds a lookup takes is cheaper than the database you take down with a guessed PID.
🎯 Key Takeaway
Probe before start, kill the named PID, verify empty — then bind once and record the port.
● Production incidentPOST-MORTEMseverity: high

Reload Children Stacked 3 Deep and Flapped Staging for 26 Minutes

Symptom
After the 3:15 p.m. staging deploy, health checks alternated pass-fail for 26 minutes and 9 start attempts exited with [Errno 98] Address already in use on port 8000. The app logs showed zero request errors, but ps aux inside the container revealed 3 uvicorn processes fighting over one port.
Assumption
The team assumed --reload was harmless in staging because it sped up demos, and code review treated the Dockerfile CMD as dev-only though staging ran the same image. Nobody checked ps aux inside the container, where 3 reloader generations were already stacked.
Root cause
The staging image launched uvicorn app:app --reload, and each deploy plus each file sync left a reloader child holding port 8000 inside the container. The new parent then crashed with Errno 98 on bind, the orchestrator retried, and 3 generations piled up — ps aux showed 3 uvicorn processes for one container. Half the health checks hit the dying holder and failed, flapping staging for 26 minutes across 9 failed starts.
Fix
The fix touched 2 files and took 18 minutes. The Dockerfile CMD lost --reload in favor of a single uvicorn app:app --host 0.0.0.0 --port 8000 --workers 2, and compose gained distinct host ports plus a health check on /health. A deploy guard now runs ss -tlnp before start and fails fast with the holder PID. Staging went green on the next deploy and stayed up through 40 code pushes.
Key lesson
  • Never ship --reload past local dev, since each save stacks a child that can hold the port the next parent needs.
  • Give every container its own host port and health check, so a bind collision fails loudly instead of flapping silently.
  • Probe the port with ss before starting the server, because a 5-second holder check beats 26 minutes of restart guessing.
Production debug guideFive bind failures that cover most uvicorn pages — each with the exact command that names the holder.5 entries
Symptom · 01
uvicorn exits instantly with Errno 48/98 on port 8000
→
Fix
Name the holder with lsof -i :8000 -sTCP:LISTEN and note the PID and command. Stop exactly that process with kill <PID>, verify with lsof -i :8000 -sTCP:LISTEN showing empty, then start uvicorn app:app --host 0.0.0.0 --port 8000 once.
Symptom · 02
--reload restart fails right after a code save
→
Fix
List survivors with ps aux | grep uvicorn | grep -v grep and kill each stale PID with kill <PID>. Confirm one listener with ss -tlnp | grep 8000, then restart a single uvicorn app:app --reload outside Docker only.
Symptom · 03
No process holds the port but bind still fails
→
Fix
Inspect socket state with ss -tanp | grep 8000 and look for TIME-WAIT with no PID column. Wait 60 seconds and retry, or boot once on uvicorn app:app --port 8001 while the old socket drains.
Symptom · 04
Second compose service exits with address-in-use
→
Fix
Show host mappings with docker ps --format '{{.Names}} {{.Ports}}' | grep 8000 to spot the double-mapped service. Edit compose so each service has its own host port, then docker compose up -d --force-recreate.
Symptom · 05
Multi-worker setup binds once then crashes 3 workers
→
Fix
Check listeners with ss -tlnp | grep 8000 and workers with ps -o pid,ppid,command -C uvicorn. Replace 4 separate commands with one uvicorn app:app --workers 4 --port 8000 so children share a single accepted socket.
Uvicorn Address-in-Use Causes at a Glance
Root CauseHow to ConfirmFixPrevention
Stale uvicorn process still boundlsof -i :8000 -sTCP:LISTEN prints a PID running your appStop it with kill <PID> then restart uvicornStop servers with Ctrl-C; use a process manager
--reload child survived restartps aux | grep uvicorn shows 2+ processes on one portKill all children, restart once without stacking savesNever use --reload in Docker or production
TIME_WAIT from a just-closed socketss -tanp | grep 8000 shows TIME-WAIT and no PIDWait 60s or restart on --port 8001 temporarilyUse --workers on one listener instead of restarts
Two services mapped to one host portdocker ps shows two containers publishing 8000Give each service its own host portLint compose ports in CI before deploy
⚙ Quick Reference
4 commands from this guide
FileCommand / CodePurpose
port_bind_demo.pysrv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)What Address-in-Use Means
port_holder_lookup.pyport = "8000"Stale Process Still Bound
reload_children_audit.pyout = subprocess.run(--reload Children That Outlive Their Parent and Keep the Por
reuseaddr_timewait.pysrv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)SO_REUSEADDR and TIME_WAIT

Key takeaways

1
Address-in-use means one socket already owns the port
name the holder with lsof or ss before killing anything.
2
Stale dev servers and --reload children are the top holders; one kill by PID beats blanket pkill.
3
TIME_WAIT has no PID and clears in ~60s
wait it out or restart once on --port 8001.
4
Never use --reload in Docker or production; it stacks children that fight over one port.
5
Share ports with --workers on one listener, not with separate commands racing to bind.
6
Give each compose service its own host port and lint the mappings in CI.

Common mistakes to avoid

5 patterns
×

Running kill -9 on the wrong PID and taking down the database

Symptom
Port 8000 frees up but Postgres on 5432 also dies, because kill -9 hit a PID copied from the wrong lsof line.
Fix
Remove --reload from the container command and keep one server per port. Use restart policies for crash recovery instead.
×

Leaving --reload on inside Docker where restarts stack children

Symptom
Each code save spawns a new reloader child while the old one keeps port 8000, so the third save fails with address-in-use inside the container.
Fix
Run ps aux | grep uvicorn | grep -v grep and kill leftovers by PID, or use a process manager that reaps children on stop.
×

Restarting instantly and colliding with your own TIME_WAIT socket

Symptom
ss -tanp | grep 8000 shows TIME-WAIT with no PID, yet the immediate restart still refuses to bind on the same port.
Fix
Wait 60 seconds for TIME_WAIT to clear, or set a different port for the second instance with --port 8001. Reserve reuse flags for workers that explicitly share.
×

Mapping two compose services to the same host port

Symptom
The second docker compose up service exits with address-in-use because both map 8000:8000 onto the host's single port 8000.
Fix
Give each service its own port in compose: web on 8000, api on 8001. Check docker ps --format '{{.Names}} {{.Ports}}' for collisions before up.
×

Starting 4 uvicorn processes on one port without a shared socket

Symptom
Worker 1 binds fine and workers 2-4 crash with address-in-use, since plain uvicorn has no shared-socket handoff between separate commands.
Fix
Run workers behind one listener: uvicorn app:app --workers 4 on one port, or put Nginx in front. Don't bind 4 processes to port 8000 directly.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What does [Errno 48] address already in use mean?
Q02JUNIOR
How do you find which process holds port 8000?
Q03SENIOR
Why does --reload cause address-in-use after code saves?
Q04SENIOR
ss shows no PID but the port still won't bind — why?
Q05SENIOR
Why does --workers 4 share a port but 4 commands collide?
Q01 of 05JUNIOR

What does [Errno 48] address already in use mean?

ANSWER
One socket already owns that IP and port, so the OS refuses a second bind. Run lsof -i :8000 to name the holder PID, stop it with kill, then restart. The error names the conflict, not broken app code.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Is address-in-use a bug in my FastAPI code?
02
Should I use lsof or ss to find the holder?
03
Does SO_REUSEADDR let two servers share a port?
04
Can TIME_WAIT block a restart with no process running?
05
Why do 4 workers on one port fail but --workers 4 works?
06
How do I stop compose services colliding on 8000?
N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Everything here is grounded in real deployments.

Follow
✓ Verified
production tested
September 24, 2026
last updated
1,942
articles · all by Naren
🔥

That's Web. Mark it forged?

5 min read · try the examples if you haven't

←
Previous
Flask App Context Fix
3 / 3 · Web
Next
pip Distutils Uninstall Fix
→