If you have ever used FastAPI or Starlette, you've already heard of Uvicorn, as it is the server that runs your app. However the majority of tutorials only discuss it as a side effect of one-liner. In this article, we'll dive deep – into the internals of Uvicorn, into correct configuration of Uvicorn and into deployment of Uvicorn in production the right way.
What Is Uvicorn?
Uvicorn is an ASGI (Asynchronous Server Gateway Interface) server implementation for Python, based on uvloop and httptools. It's designed specifically for running async Python web frameworks like FastAPI, Starlette, Django Channels, and Quart.
Why WSGI vs ASGI?
The classic server interface used in python is WSGI (which is used in Flask and Django). It's a synchronous request, one by one per worker. It is good for conventional applications and doesn't work well with:
- WebSockets
- Long-polling
- Async database calls
- High-concurrency workloads ASGI solves this. Uvicorn is the fastest server implementation for it, its the async version of WSGI.
Traditional Stack: Nginx → Gunicorn (WSGI) → Flask/Django
Modern Stack: Nginx → Uvicorn (ASGI) → FastAPI/Starlette
Installation
pip install uvicorn
pip install "uvicorn[standard]" //with high performance
The [standard] extras install:
- uvloop: A drop-in replacement for Python's default event loop, written in Cython. ~2-4x faster.
- httptools: A fast HTTP parser (used by Node.js). Much faster than the pure-Python fallback.
- websockets: WebSocket support
- watchfiles: File watching for --reload
# Run app:app (file: app.py, object: app)
uvicorn app:app
# With auto-reload for development
uvicorn app:app --reload
# Bind to a specific host and port
uvicorn app:app --host 0.0.0.0 --port 8080
# Verbose logging
uvicorn app:app --log-level debug
Your app.py just needs to expose an ASGI callable:
# app.py — raw ASGI, no framework needed
async def app(scope, receive, send):
assert scope["type"] == "http"
await send({
"type": "http.response.start",
"status": 200,
"headers": [[b"content-type", b"text/plain"]],
})
await send({
"type": "http.response.body",
"body": b"Hello from raw ASGI!",
})
Configuration
Workers
# Single worker (default)- fine for development
uvicorn app:app
# Multiple workers — for production on a single machine
uvicorn app:app --workers 4
The general rule of thumb for worker count: (2 × CPU cores) + 1. On a 4-core machine, that's 9 workers.
Timeouts and Limits
uvicorn app:app \
--timeout-keep-alive 5 \ # seconds to wait for next request on keep-alive connection
--limit-concurrency 100 \ # max concurrent connections before returning 503
--limit-max-requests 1000 # restart worker after N requests (memory leak prevention)
Programmatic Configuration
When you just want to control Uvicorn from Python code, not the CLI; useful for custom startup logic, testing, or process managers.
import uvicorn
if __name__ == "__main__":
uvicorn.run(
"app:app",
host="0.0.0.0",
port=8000,
workers=4,
log_level="info",
access_log=True,
reload=False,
)
Or with a Config object for more control:
import uvicorn
from uvicorn.config import Config
from uvicorn.main import Server
config = Config(
app="app:app",
host="0.0.0.0",
port=8000,
log_level="warning",
workers=2,
)
server = Server(config=config)
# Now you can do things before/after serving
import asyncio
asyncio.run(server.serve())
Logging Configuration
Uvicorn's default logging is decent, but in production you'll want structured JSON logs:
import uvicorn
import logging
LOG_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"json": {
"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
"format": "%(asctime)s %(name)s %(levelname)s %(message)s",
}
},
"handlers": {
"default": {
"class": "logging.StreamHandler",
"formatter": "json",
"stream": "ext://sys.stdout",
}
},
"loggers": {
"uvicorn": {"handlers": ["default"], "level": "INFO"},
"uvicorn.error": {"handlers": ["default"], "level": "INFO"},
"uvicorn.access": {"handlers": ["default"], "level": "INFO"},
},
}
uvicorn.run("app:app", log_config=LOG_CONFIG)
Production Deployment Patterns
Pattern 1: Uvicorn + Gunicorn (Recommended for Most Cases)
Gunicorn is a battle-tested process manager. Use it to manage Uvicorn workers:
pip install gunicorn
gunicorn app:app \
-w 4 \
-k uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000 \
--timeout 60 \
--keep-alive 5 \
--access-logfile - \
--error-logfile -
This gives you Gunicorn's robust process management (signal handling, worker restarts, graceful shutdown) with Uvicorn's async performance.
Pattern 2: Uvicorn Behind Nginx
upstream fastapi_app {
server 127.0.0.1:8000;
server 127.0.0.1:8001; # multiple Uvicorn instances
}
server {
listen 80;
server_name api.yourdomain.com;
location / {
proxy_pass http://fastapi_app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Pattern 3: Systemd Service
# /etc/systemd/system/myapp.service
[Unit]
Description=My FastAPI App
After=network.target
[Service]
Type=exec
User=www-data
WorkingDirectory=/var/www/myapp
ExecStart=/var/www/myapp/venv/bin/gunicorn app:app \
-w 4 \
-k uvicorn.workers.UvicornWorker \
--bind unix:/run/myapp.sock \
--timeout 60
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
systemctl enable myapp
systemctl start myapp
Pattern 4: Docker
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
WebSocket Support
Uvicorn handles WebSockets natively; no extra config needed:
from fastapi import FastAPI, WebSocket
app = FastAPI()
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
while True:
data = await websocket.receive_text()
await websocket.send_text(f"Echo: {data}")
This works seamlessly because Uvicorn handles both HTTP and WebSocket connections through the ASGI interface.
Lifespan Events: Startup and Shutdown
Do you need to initialize a database pool or ML model when the server starts? Use lifespan events:
from contextlib import asynccontextmanager
from fastapi import FastAPI
import asyncpg
db_pool = None
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
global db_pool
db_pool = await asyncpg.create_pool("postgresql://user:pass@localhost/db")
print("Database pool created")
yield
# Shutdown
await db_pool.close()
print("Database pool closed")
app = FastAPI(lifespan=lifespan)
Uvicorn calls these at the right moment during server start and graceful shutdown.
Performance Tips
- Always use
uvicorn[standard]— the uvloop + httptools combo is a free 2-4x speedup - Use
--limit-max-requestsin production to prevent memory leaks from gradual bloat - Unix sockets over TCP when Nginx and Uvicorn are on the same machine — reduces overhead
- Profile before scaling — adding workers helps CPU-bound work; for I/O-bound async apps, one worker with high concurrency often beats many workers
Summary
Uvicorn isn't just a "uvicorn main:app" one-liner. It's a production-grade ASGI server with rich configuration options, WebSocket support, lifecycle events, and proven deployment patterns.
Getting familiar with these options means you'll be ready to take your async Python apps from laptop to production confidently.
Top comments (0)