Uvicorn Address in Use: Free Port 8000 Fast
Free the port with lsof and kill, then restart uvicorn cleanly.
20+ years shipping production Python across data and backend systems. Everything here is grounded in real deployments.
- ✓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
- 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.
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.
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.
--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.
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.
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.
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.
Reload Children Stacked 3 Deep and Flapped Staging for 26 Minutes
[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.ps aux inside the container, where 3 reloader generations were already stacked.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.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.- 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.
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.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.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.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.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.| File | Command / Code | Purpose |
|---|---|---|
| port_bind_demo.py | srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | What Address-in-Use Means |
| port_holder_lookup.py | port = "8000" | Stale Process Still Bound |
| reload_children_audit.py | out = subprocess.run( | --reload Children That Outlive Their Parent and Keep the Por |
| reuseaddr_timewait.py | srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | SO_REUSEADDR and TIME_WAIT |
Key takeaways
Common mistakes to avoid
5 patternsRunning kill -9 on the wrong PID and taking down the database
kill -9 hit a PID copied from the wrong lsof line.Leaving --reload on inside Docker where restarts stack children
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
ss -tanp | grep 8000 shows TIME-WAIT with no PID, yet the immediate restart still refuses to bind on the same port.--port 8001. Reserve reuse flags for workers that explicitly share.Mapping two compose services to the same host port
docker compose up service exits with address-in-use because both map 8000:8000 onto the host's single port 8000.docker ps --format '{{.Names}} {{.Ports}}' for collisions before up.Starting 4 uvicorn processes on one port without a shared socket
uvicorn app:app --workers 4 on one port, or put Nginx in front. Don't bind 4 processes to port 8000 directly.Interview Questions on This Topic
What does [Errno 48] address already in use mean?
Frequently Asked Questions
20+ years shipping production Python across data and backend systems. Everything here is grounded in real deployments.
That's Web. Mark it forged?
5 min read · try the examples if you haven't