Lỗi Này Là Gì
Bạn khởi động server. Trước khi nó kịp boot xong, bạn thấy dòng này:
OSError: [Errno 98] Address already in use
Flask hiển thị như sau:
$ 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 và macOS báo Errno 98. Windows thì trả về WinError 10048 — cùng một vấn đề, chỉ khác tên gọi.
Nguyên Nhân
Có thứ gì đó đã chiếm port của bạn trước khi server kịp khởi động. Các nguyên nhân thường gặp:
- Một tiến trình server trước đó chưa thoát hoàn toàn — bạn nhấn Ctrl+C đóng terminal nhưng socket vẫn còn mở
- Một dev server khác đang chạy trong tab mà bạn quên mất
- Một service hệ thống đang chiếm port đó (trên macOS Monterey trở lên, AirPlay Receiver âm thầm chiếm port 5000)
- Một tiến trình uvicorn hoặc gunicorn zombie vẫn đang giữ socket sau khi crash —
ps aux | grep uvicornsẽ phát hiện ra nó
Cách Xử Lý Nhanh: Kill Tiến Trình Đang Chặn
Bước 1 — Tìm xem cái gì đang chiếm port
Linux/macOS:
# Thay 5000 bằng port thực tế của bạn
lsof -ti:5000
Lệnh này in thẳng PID ra. Không có output nghĩa là port đang trống — lỗi đến từ chỗ khác.
Thích dùng ss? Đây là lệnh tương đương trên Linux:
ss -lptn 'sport = :5000'
Trên Windows (PowerShell):
netstat -ano | findstr :5000
Bước 2 — Kill tiến trình đó
Linux/macOS — pipe lsof thẳng vào kill:
kill -9 $(lsof -ti:5000)
Đã có PID từ ss? Kill trực tiếp:
kill -9 <PID>
Windows (PowerShell):
Stop-Process -Id <PID> -Force
Khởi động lại server và mọi thứ sẽ hoạt động bình thường.
macOS: AirPlay Receiver chiếm port 5000
Từ macOS Monterey, AirPlay Receiver âm thầm bind port 5000 khi khởi động máy. Nó sẽ không xuất hiện trong danh sách tiến trình thông thường. Để tắt tính năng này, vào System Settings → General → AirDrop & Handoff → AirPlay Receiver. Hoặc đơn giản hơn, chuyển Flask app sang port khác:
flask run --port 5001
Giải Pháp Lâu Dài
Giải pháp 1: Dùng SO_REUSEADDR (Flask)
Dev server của Flask đã bật tùy chọn này mặc định, nhưng raw socket và custom server thì không. SO_REUSEADDR báo cho OS giải phóng port ngay khi tiến trình thoát — thay vì giữ nó ở trạng thái TIME_WAIT tới 60 giây:
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))
Giải pháp 2: Cấu hình port qua biến môi trường
Hardcode port 5000 cho ba project khác nhau là rước họa vào thân. Một biến môi trường là đủ để giải quyết.
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
Giờ bạn có thể chạy song song hai project mà không cần động vào code:
PORT=5001 python app.py
PORT=8001 uvicorn main:app --reload
Giải pháp 3: Tự động tìm port trống (môi trường dev)
Đang chạy năm project local cùng lúc và không quan tâm port nào là bao nhiêu? Để OS tự chọn:
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)
Đừng dùng cách này cho production — URL sẽ thay đổi mỗi lần restart. Chỉ phù hợp khi dev local với nhiều project cùng lúc.
Giải pháp 4: Script dọn port trước khi khởi động
Restart cùng một dev server hàng chục lần mỗi ngày? Thêm bước dọn port vào script khởi động để tiến trình cũ không bao giờ chặn bạn nữa:
#!/bin/bash
PORT=8000
# Giải phóng port trước khi khởi động
fuser -k ${PORT}/tcp 2>/dev/null || true
uvicorn main:app --host 0.0.0.0 --port $PORT --reload
fuser -k gửi SIGKILL tới bất cứ thứ gì đang giữ port. || true giúp script không bị lỗi khi không có tiến trình nào đang chiếm port.
Kiểm Tra Sau Khi Xử Lý
Trước khi restart, hãy xác nhận port đã thực sự trống:
# Không có output nghĩa là port đã trống
lsof -ti:5000
Flask khởi động thành công trông như sau:
* Running on http://127.0.0.1:5000
* Debugger is active!
uvicorn khởi động thành công:
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: Started reloader process [12345]
Vẫn thấy lỗi sau khi đã kill tiến trình? Hãy chờ 30 giây. OS giữ socket ở trạng thái TIME_WAIT — đây là cơ chế TCP để bắt các gói tin đến trễ. Đặt SO_REUSEADDR sẽ bỏ qua hoàn toàn thời gian chờ này.

