The Error
You run the server. Before it finishes booting, you see this:
OSError: [Errno 98] Address already in use
Flask shows it like this:
$ python app.py
* Running on http://127.0.0.1:5000
OSError: [Errno 98] Address already in use
FastAPI/uvicorn:
$ uvicorn main:app --reload
ERROR: [Errno 98] Address already in use
Linux and macOS throw Errno 98. Windows gives you WinError 10048 instead β same problem, different label.
Why It Happens
Something else grabbed your port before your server could. The usual suspects:
- A previous server instance that didn't exit cleanly β Ctrl+C closed your terminal but the socket stayed open
- Another dev server running in a tab you forgot about
- A system service sitting on that port (on macOS Monterey+, AirPlay Receiver quietly claims port 5000)
- A zombie uvicorn or gunicorn worker still holding the socket after a crash β
ps aux | grep uvicornwill expose it
Quick Fix: Kill the Blocking Process
Step 1 β Find what's on the port
Linux/macOS:
# Replace 5000 with your actual port
lsof -ti:5000
That prints the PID directly. No output means the port is free β the error is coming from somewhere else.
Prefer ss? Here's the Linux equivalent:
ss -lptn 'sport = :5000'
On Windows (PowerShell):
netstat -ano | findstr :5000
Step 2 β Kill it
Linux/macOS β pipe lsof straight into kill:
kill -9 $(lsof -ti:5000)
Got the PID from ss? Kill it directly:
kill -9 <PID>
Windows (PowerShell):
Stop-Process -Id <PID> -Force
Restart your server and it should come up clean.
macOS: AirPlay Receiver on port 5000
Since macOS Monterey, AirPlay Receiver silently binds port 5000 at boot. It won't show up in a normal process list. To disable it, go to System Settings β General β AirDrop & Handoff β AirPlay Receiver. Or just move your Flask app to a different port:
flask run --port 5001
Permanent Fix Options
Option 1: Use SO_REUSEADDR (Flask)
Flask's dev server sets this by default, but raw sockets and custom servers don't. SO_REUSEADDR tells the OS to release the port immediately when your process exits β instead of holding it in TIME_WAIT for up to 60 seconds:
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('0.0.0.0', 5000))
Option 2: Configure the port via environment variable
Hardcoding port 5000 across three different projects is a collision waiting to happen. One env var fixes it.
Flask (app.py):
import os
from flask import Flask
app = Flask(__name__)
if __name__ == '__main__':
port = int(os.environ.get('PORT', 5000))
app.run(host='0.0.0.0', port=port)
FastAPI/uvicorn (start.sh):
PORT=${PORT:-8000} uvicorn main:app --host 0.0.0.0 --port $PORT --reload
Now you can run two projects side by side without touching code:
PORT=5001 python app.py
PORT=8001 uvicorn main:app --reload
Option 3: Auto-find a free port (dev environments)
Running five local projects at once and don't care which port each gets? Let the OS pick one:
import socket
from flask import Flask
def find_free_port():
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('', 0))
return s.getsockname()[1]
app = Flask(__name__)
if __name__ == '__main__':
port = find_free_port()
print(f'Starting on port {port}')
app.run(port=port)
Skip this for production β your URL changes every restart. Fine for local dev juggling.
Option 4: Kill-on-startup wrapper script
Restarting the same dev server dozens of times a day? Add a port-clearing step to your launch script so stale processes never block you:
#!/bin/bash
PORT=8000
# Free the port before starting
fuser -k ${PORT}/tcp 2>/dev/null || true
uvicorn main:app --host 0.0.0.0 --port $PORT --reload
fuser -k sends SIGKILL to whatever holds the port. The || true keeps the script from failing when nothing is there.
Verify the Fix
Before restarting, confirm the port is actually free:
# Should return nothing if port is free
lsof -ti:5000
A clean Flask startup looks like:
* Running on http://127.0.0.1:5000
* Debugger is active!
Clean uvicorn startup:
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: Started reloader process [12345]
Still seeing the error after killing the process? Wait 30 seconds. The OS holds sockets in TIME_WAIT state β a TCP mechanism to catch delayed packets in flight. Setting SO_REUSEADDR bypasses this wait entirely.

