Running Python ASGI Apps with Uvicorn: Features, Configuration, and Deployment
Overview
Uvicorn is a high‑performance ASGI server that emphasizes simplicity and speed. It builds on event-loop and HTTP parsing libraries optimized in C, making it a strong choice for serving modern asynchronous Python web applications.
- uvloop: A drop-in replacement for Python’s default asyncio event loop, implemented in Cython and typically 2–4× faster than the stock loop.
- httptools: Python bindings to the Node.js HTTP parser, providing fast HTTP/1.1 parsing.
ASGI in a Nutshell
ASGI (Asynchronous Server Gateway Interface) is the standard interface between async Python applications and servers. It enables servers and frameworks to interoperate over protocols like HTTP/1.1 and WebSocket, and is the successor to WSGI in asynchronous conteexts.
What Uvicorn Supports
- HTTP/1.1
- WebSocket
- HTTP/2 support is not native at the time of writing; check the Uvicorn changelog for updates
Installation
pip install uvicorn
Minimal ASGI Application
Create example.py with a small ASGI 3 application:
# example.py
async def app(scope, receive, send):
if scope["type"] != "http":
return
body = b"Hello from ASGI via Uvicorn"
headers = [
(b"content-type", b"text/plain"),
(b"content-length", str(len(body)).encode()),
]
await send({
"type": "http.response.start",
"status": 200,
"headers": headers,
})
await send({
"type": "http.response.body",
"body": body,
})
Start the server from the command line:
uvicorn example:app
Starting Uvicorn Programmatically
# run_app.py
import uvicorn
async def app(scope, receive, send):
... # your ASGI app
if __name__ == "__main__":
uvicorn.run("example:app", host="127.0.0.1", port=5000, log_level="info")
Note: When using features like reload or multiple workers, pass the application as an import string ("module:attribute").
Using Uvicorn with FastAPI
# main.py
from fastapi import FastAPI
api = FastAPI()
@api.get("/")
async def hello():
return {"message": "Hello World"}
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:api", host="0.0.0.0", port=8000)
How uvicorn.run Handles Reload and Workers (Conceptual)
# Pseudocode — illustrates behavior, not source code
def run(app, **kwargs):
cfg = Config(app, **kwargs)
srv = Server(cfg)
# Require import string for reload/workers
if (cfg.reload or cfg.workers and cfg.workers > 1) and not isinstance(app, str):
# log warning + exit
raise SystemExit(1)
if cfg.should_reload:
sock = cfg.bind_socket()
StatReload(cfg, target=srv.run, sockets=[sock]).run()
elif cfg.workers and cfg.workers > 1:
sock = cfg.bind_socket()
Multiprocess(cfg, target=srv.run, sockets=[sock]).run()
else:
srv.run()
Config Highlights
Uvicorn’s Config exposes many knobs for tuning behavior. Commonly used options include:
class Config:
def __init__(
self,
app,
host: str = "127.0.0.1",
port: int = 8000,
loop: str = "auto",
http: str = "auto",
ws: str = "auto",
lifespan: str = "auto",
log_level: str | None = None,
access_log: bool = True,
reload: bool = False,
reload_dirs: list[str] | None = None,
workers: int | None = None,
proxy_headers: bool = True,
forwarded_allow_ips: str | None = None,
root_path: str = "",
timeout_keep_alive: int = 5,
ssl_keyfile: str | None = None,
ssl_certfile: str | None = None,
headers: list[tuple[str, str]] | None = None,
# ... many more
):
...
Programmatic example with common flags:
import uvicorn
uvicorn.run(
"main:api",
host="127.0.0.1",
port=8000,
reload=True,
log_level="debug",
)
If you enable reload or multiple workers in code, provide the app as "module:attribute":
# Correct for reload/workers
uvicorn.run("myservice.main:app", reload=True, workers=1)
Command-Line Essentials
Useful command-line flags (subset):
- --host, --port: bind address and port
- --reload, --reload-dir: auto-reload on code changes
- --workers: number of worker processes (not valid with --reload)
- --loop [auto|asyncio|uvloop|iocp]: event loop policy
- --http [auto|h11|httptools]: HTTP implementation
- --ws [auto|none|websockets|wsproto]: WebSocket implementation
- --lifespan [auto|on|off]: lifespan protocol handling
- --interface [auto|asgi3|asgi2|wsgi]: application interface
- --log-level, --log-config, --no-access-log
- --proxy-headers, --forwarded-allow-ips, --root-path
- --limit-concurrency, --limit-max-requests, --backlog
- --timeout-keep-alive
- --ssl-keyfile, --ssl-certfile, --ssl-ca-certs, --ssl-ciphers
Get the full list with:
uvicorn --help
Running Multiple Processes
Uvicorn can spawn multiple workers via --workers, but it does not act as a full process manager. For production, pair Uvicorn’s workers with a process manager (such as Gunicorn, Supervisor, Circus, or a container orchestration platform).
Gunicorn + Uvicorn Workers
Gunicorn is a mature process manager and web server. Uvicorn ships worker classes that plug into Gunicorn so you get Uvicorn’s performance with Gunicorn’s management features.
gunicorn example:app -w 4 -k uvicorn.workers.UvicornWorker
For environments where pure-Python is preferred (e.g., PyPy), use the H11-based worker:
gunicorn -w 4 -k uvicorn.workers.UvicornH11Worker example:app
Note: Some Uvicorn-specific switches (for example, certain concurrency limits) may not map directly to Gunicorn.
Supervisor Configuration
You can hand off the listening socket to Uvicorn via a file descriptor exposed by Supervisor. A basic configuration using an fcgi-program entry might look like this:
[supervisord]
[fcgi-program:asgi]
socket = tcp://127.0.0.1:8000
command = /path/to/venv/bin/uvicorn --fd 0 myapp.main:app
numprocs = 4
process_name = asgi-%(process_num)d
stdout_logfile = /dev/stdout
stdout_logfile_maxbytes = 0
Start Supervisor (e.g., supervisord -n) to launch the workers.
Circus Configuration
Circus can also pass an existing socket to Uvicorn:
[watcher:web]
cmd = /path/to/venv/bin/uvicorn --fd $(circus.sockets.web) myapp.main:app
use_sockets = True
numprocesses = 4
[socket:web]
host = 0.0.0.0
port = 8000
Run with:
circusd circus.ini
Reverse Proxy with Nginx
Place Nginx in front of Uvicorn for TLS termination, buffering, or load balancing. Forward the original client information so your app can reconstruct scheme and client IPs.
http {
upstream uvicorn_upstream {
# Example using a Unix socket; TCP endpoints also work
server unix:/tmp/uvicorn.sock;
}
server {
listen 80;
server_name example.com;
client_max_body_size 4G;
location /static/ {
alias /path/to/app/static/;
}
location / {
proxy_pass http://uvicorn_upstream;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
proxy_buffering off;
}
}
}
When proxying, consider starting Uvicorn with --proxy-headers and set --forwarded-allow-ips appropriately so forwarded headers are trusted.
HTTPS
To serve HTTPS directly from Uvicorn, provide a certificate and private key. In production, obtain certificates via Let’s Encrypt (e.g., with certbot). For local development, tools like mkcert can generate trusted certificates.
uvicorn example:app --port 5000 \
--ssl-keyfile ./key.pem \
--ssl-certfile ./cert.pem
HTTPS with Gunicorn + Uvicorn Worker
gunicorn example:app \
-k uvicorn.workers.UvicornWorker \
--keyfile ./key.pem \
--certfile ./cert.pem