ASGI Server for Python
Uvicorn
Handbook
A complete reference for running async Python web applications — covering ASGI fundamentals, CLI options, production deployment with Gunicorn, Docker, Nginx, and performance tuning.
ASGI / asyncio
FastAPI & Starlette
HTTP/1.1 + HTTP/2
WebSockets
Gunicorn workers
Production ready
Uvicorn is a production-grade, minimal ASGI (Asynchronous Server Gateway Interface) server for Python. It acts as the bridge between your async Python web application and the outside world — it listens for HTTP/WebSocket connections and passes them to your app following the ASGI spec.
It is built on two C-extension libraries: uvloop (a fast asyncio event loop based on libuv) and httptools (a fast HTTP parser based on Node.js's http-parser). This gives it throughput comparable to Go and Node.js.
ASGI Server
Uvicorn implements the ASGI spec — it runs your async Python app the same way Apache runs PHP. It is the runtime layer, not the framework.
Built for async
Designed from the ground up for Python's asyncio. Natively supports async def views, WebSockets, Server-Sent Events, and long-lived connections.
Framework agnostic
Works with any ASGI-compatible framework: FastAPI, Starlette, Django (≥3.0), Quart, Litestar, BlackSheep, and more.
How Uvicorn fits in the stack
🌍ClientBrowser / API
→
🌐Reverse proxyNginx / Caddy
→
🦄Process mgrGunicorn
→
⚡ASGI serverUvicorn worker
→
🧩FrameworkFastAPI / Starlette
→
📦App logicYour code
🆕
Uvicorn v0.30+ introduces lifespan protocol improvements, HTTP/2 support via h2, and first-class support for uvicorn[standard] extras which bundles uvloop, httptools, and websockets for maximum performance.
Exceptional throughput
uvloop replaces asyncio's default event loop with a Cython-based implementation of libuv, delivering 2–4× faster I/O performance compared to the stdlib event loop.
True async concurrency
Unlike WSGI servers (Gunicorn, uWSGI) that rely on threads or processes per request, Uvicorn can handle thousands of concurrent connections on a single thread via coroutines.
WebSocket & SSE support
First-class, spec-compliant support for WebSocket connections and Server-Sent Events — no bolt-on solutions needed.
Hot reload for dev
--reload watches your source files and restarts the server on changes. Supports watchfiles for faster, more precise detection.
Minimal footprint
Uvicorn itself has very few dependencies. It does one thing well — serve ASGI apps — and composes cleanly with Gunicorn for process management.
Industry standard
The default server for FastAPI (used at Uber, Netflix, Microsoft) and Starlette. Used in production at massive scale worldwide.
✅
Benchmark context: In independent TechEmpower benchmarks, FastAPI + Uvicorn consistently ranks among the top Python web frameworks — outperforming Django + Gunicorn by 3–10× on JSON serialisation and plain-text responses, while matching Node.js Express in many scenarios.
✅ Use Uvicorn when…
- You're building with FastAPI, Starlette, or any ASGI framework
- Your app is I/O-heavy (DB queries, HTTP calls, file reads)
- You need WebSocket or SSE support
- You want high concurrency without scaling to many processes
- You're doing async background tasks in your web process
- You're building APIs, streaming endpoints, or chat-like systems
❌ Consider alternatives when…
- Your codebase is entirely synchronous Django/Flask (use Gunicorn + gevent)
- You need battle-hardened WSGI compatibility (uWSGI)
- You have CPU-heavy workloads where more processes matter more than async
- You're on a platform where uvloop isn't available (pure Python fallback is slower)
- You need Mod_WSGI Apache integration (ASGI isn't supported there)
ℹ️
WSGI vs ASGI: Django 3.0+ and Flask 2.0+ both support ASGI, so you can use Uvicorn with them too. However, if your views are synchronous, the performance benefit is minimal — the async layer adds overhead for purely sync code.
# Minimal install (pure Python fallback, no uvloop/httptools)
pip install uvicorn
# RECOMMENDED: full performance extras
pip install "uvicorn[standard]"
# Installs: uvicorn + uvloop + httptools + websockets + watchfiles
# With Gunicorn for production process management
pip install "uvicorn[standard]" gunicorn
# Verify install
uvicorn --version
# → Running uvicorn 0.32.0 with CPython 3.12.0 on Linux
What each extra installs
| Package | Purpose | Included in [standard] |
uvloop | Fast asyncio event loop (C extension, 2–4× faster). Falls back to asyncio on Windows. | ✅ |
httptools | Fast HTTP/1.1 parser (C extension, from Node.js http-parser) | ✅ |
websockets | WebSocket protocol implementation | ✅ |
watchfiles | Efficient file watching for --reload (replaces older watchdog) | ✅ |
python-dotenv | Load .env files for environment configuration | ❌ (manual) |
h2 | HTTP/2 protocol support | ❌ (manual) |
⚠️
Windows note: uvloop does not support Windows. On Windows, Uvicorn falls back to the standard asyncio event loop automatically. Performance is still acceptable but lower than Linux/macOS. For production, deploy on Linux.
Minimal ASGI app (no framework)
# A raw ASGI app — 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, Uvicorn!',
})
uvicorn main:app
# → INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
With FastAPI (typical usage)
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
# Development — with auto-reload
uvicorn main:app --reload
# Custom host and port
uvicorn main:app --host 0.0.0.0 --port 8080 --reload
# Production — multiple workers
uvicorn main:app --workers 4 --host 0.0.0.0 --port 8000
Running programmatically
import uvicorn
if __name__ == "__main__":
uvicorn.run(
"main:app", # module:attribute string (enables reload)
host="0.0.0.0",
port=8000,
reload=True, # dev only
log_level="info",
)
# Or pass the app object directly (reload not available)
from main import app
uvicorn.run(app, host="127.0.0.1", port=8000)
ASGI is the successor to WSGI. Where WSGI is synchronous and request/response only, ASGI is async and supports long-lived connections (WebSockets, SSE, HTTP/2 push). Every ASGI app is a callable with this signature:
# Every ASGI app is an async callable(scope, receive, send)
# scope → dict describing the connection
# receive → awaitable — yields incoming events (request body, WS messages)
# send → awaitable — sends events back (response start, body, WS message)
async def app(scope, receive, send):
if scope['type'] == 'http':
# scope keys: method, path, query_string, headers, client, server
body_event = await receive() # {'type': 'http.request', 'body': b'...', 'more_body': False}
await send({'type': 'http.response.start', 'status': 200,
'headers': [[b'content-type', b'application/json']]})
await send({'type': 'http.response.body', 'body': b'{"ok": true}'})
elif scope['type'] == 'websocket':
await send({'type': 'websocket.accept'})
event = await receive()
if event['type'] == 'websocket.receive':
await send({'type': 'websocket.send', 'text': event['text']})
elif scope['type'] == 'lifespan':
while True:
event = await receive()
if event['type'] == 'lifespan.startup':
# initialize DB pool, load models, etc.
await send({'type': 'lifespan.startup.complete'})
elif event['type'] == 'lifespan.shutdown':
# teardown gracefully
await send({'type': 'lifespan.shutdown.complete'})
return
ℹ️
Lifespan events are the ASGI way to run startup/shutdown logic — initialize database connection pools, ML models, background tasks. FastAPI exposes this via @app.on_event("startup") or the newer lifespan context manager parameter.
# FastAPI
pip install fastapi uvicorn[standard]
uvicorn main:app --reload
# Starlette
pip install starlette uvicorn[standard]
uvicorn app:app --reload
# Django (3.0+ ASGI)
pip install django uvicorn[standard]
uvicorn myproject.asgi:application --reload
# Litestar (formerly Starlite)
pip install litestar uvicorn[standard]
uvicorn app:app --reload
# Quart (async Flask-like)
pip install quart uvicorn[standard]
uvicorn app:app --reload
| Framework | ASGI? | Built-in dev server | Notes |
| FastAPI | ✅ Native | Uses Uvicorn internally (fastapi dev) | Most popular combo. FastAPI wraps Starlette. |
| Starlette | ✅ Native | — | Uvicorn's closest sibling project (same author) |
| Django 3.0+ | ✅ via asgi.py | runserver (Daphne) | Point Uvicorn at myproject.asgi:application |
| Flask 2.0+ | ⚠️ Limited | Werkzeug dev server | Async views only; sync views have overhead |
| Litestar | ✅ Native | Uses Uvicorn internally | High-performance alternative to FastAPI |
| Quart | ✅ Native | Hypercorn | Flask API, async implementation |
| Sanic | ❌ Custom | Built-in server | Sanic has its own server; doesn't use ASGI/Uvicorn |
uvicorn [OPTIONS] APP
# APP format: module_path:attribute
# Examples:
uvicorn main:app # main.py, variable `app`
uvicorn mypackage.server:create_app # factory function
uvicorn app.main:app # nested module
Connection & binding
| Flag | Default | Description |
| --host TEXT | 127.0.0.1 | Bind to this host. Use 0.0.0.0 to listen on all interfaces (production). |
| --port INT | 8000 | Bind to this port. |
| --uds TEXT | — | Bind to a Unix domain socket path (e.g. /tmp/uvicorn.sock). Faster than TCP for local Nginx proxy. |
| --fd INT | — | Bind to a socket from a file descriptor (systemd socket activation). |
Workers & concurrency
| Flag | Default | Description |
| --workers INT | 1 | Number of worker processes. Rule of thumb: (2 × CPU cores) + 1. Use Gunicorn instead for better process management. |
| --loop [auto|asyncio|uvloop] | auto | Event loop implementation. auto picks uvloop if available. |
| --http [auto|h11|httptools] | auto | HTTP implementation. auto picks httptools if available. |
| --ws [auto|none|websockets|wsproto] | auto | WebSocket implementation. |
| --lifespan [auto|on|off] | auto | Control ASGI lifespan events. Set on to require startup/shutdown handlers. |
| --limit-concurrency INT | — | Maximum concurrent connections before returning 503 Service Unavailable. |
| --backlog INT | 2048 | Maximum number of connections to hold in the listen backlog. |
Development
| Flag | Default | Description |
| --reload | False | Enable auto-reload when source files change. Requires app passed as a string ("main:app"), not an import. |
| --reload-dir PATH | cwd | Directory to watch. Can be specified multiple times. Defaults to current working directory. |
| --reload-include GLOB | *.py | Glob patterns of files to include in watch. Example: *.py *.html *.jinja2 |
| --reload-exclude GLOB | — | Glob patterns to exclude from watch. Example: tests/* |
| --reload-delay FLOAT | 0.25 | Seconds to wait before triggering reload after a file change. |
Logging
| Flag | Default | Description |
| --log-level [critical|error|warning|info|debug|trace] | info | Log level for the Uvicorn logger. |
| --log-config PATH | — | Path to a JSON or YAML logging config file (Python logging dict config format). |
| --access-log / --no-access-log | True | Enable/disable HTTP access logging. Disable in production if Nginx logs requests instead. |
| --use-colors / --no-use-colors | auto | Colorized output. Disabled automatically if not writing to a TTY. |
Timeouts & limits
| Flag | Default | Description |
| --timeout-keep-alive INT | 5 | Seconds to wait for a new request on a keep-alive connection before closing it. |
| --timeout-notify INT | 30 | Seconds between SIGSIGNAL to Gunicorn (workers only). Prevents "Worker timed out" false positives. |
| --timeout-graceful-shutdown INT | — | Seconds to wait for in-flight requests to complete before forcing shutdown on SIGTERM. |
| --limit-max-requests INT | — | Max requests per worker before restarting. Useful to combat memory leaks over time. |
| --header NAME VALUE | — | Inject a custom response header on every request. E.g. --header X-Served-By uvicorn |
Uvicorn does not have a native config file format, but you can use pyproject.toml, or pass config as a Python dict via the programmatic API. Many teams use a gunicorn.conf.py when running with Gunicorn (see the Gunicorn section).
Via pyproject.toml (using Gunicorn)
[tool.uvicorn]
host = "0.0.0.0"
port = 8000
workers = 4
log_level = "info"
access_log = true
reload = false
Programmatic Config object
import uvicorn
from uvicorn.config import Config
from uvicorn.main import Server
from main import app
config = Config(
app=app,
host="0.0.0.0",
port=8000,
workers=4,
log_level="info",
access_log=True,
reload=False,
timeout_keep_alive=30,
limit_max_requests=10000, # recycle workers after 10k requests
)
server = Server(config)
# Run inside an existing event loop
import asyncio
asyncio.run(server.serve())
# Or use the simple shorthand
uvicorn.run(app, host="0.0.0.0", port=8000)
✅
Best practice: Use environment variables for environment-specific config (port, workers, log level). Keep a gunicorn.conf.py file for production settings. Avoid hardcoding host/port in application code.
Uvicorn handles concurrency through the asyncio event loop — a single process can handle thousands of concurrent I/O-bound requests. For CPU-bound scaling, use multiple worker processes.
Single worker (default)
One process, one event loop. All requests are handled concurrently via coroutines. Great for development and for apps on containers with a single vCPU. Handles thousands of simultaneous connections.
Multiple workers
Multiple OS processes, each with its own event loop. Necessary for multi-core servers. Formula: (2 × CPU cores) + 1. State is not shared between workers — use Redis/DB for session state.
# 4 workers for a 2-core machine (formula: 2 × cores + 1)
uvicorn main:app --workers 4
# Recycle workers after N requests (prevents memory leaks)
uvicorn main:app --workers 4 --limit-max-requests 10000
# Limit max concurrent connections (return 503 if exceeded)
uvicorn main:app --limit-concurrency 500
⚠️
Avoid blocking the event loop. All I/O must be async. Calling synchronous blocking functions (e.g. requests.get(), time.sleep(), CPU-heavy code) in an async def view will block the entire event loop and starve all other requests. Use httpx.AsyncClient, asyncio.sleep(), or run sync code in a thread pool via loop.run_in_executor().
import asyncio
from fastapi import FastAPI
app = FastAPI()
# ❌ WRONG — blocks event loop
@app.get("/bad")
async def bad_endpoint():
import time
time.sleep(2) # blocks ALL concurrent requests for 2 seconds
return {"status": "done"}
# ✅ CORRECT — yields control back to event loop
@app.get("/good")
async def good_endpoint():
await asyncio.sleep(2) # other requests run during this pause
return {"status": "done"}
# ✅ Run sync code without blocking (thread pool)
@app.get("/sync-safe")
async def sync_safe_endpoint():
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(None, some_blocking_function)
return {"result": result}
# Generate a self-signed cert for local dev
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes \
-subj "/CN=localhost"
# Run Uvicorn with SSL
uvicorn main:app --ssl-keyfile key.pem --ssl-certfile cert.pem --port 443
uvicorn.run(
"main:app",
host="0.0.0.0",
port=443,
ssl_keyfile="./key.pem",
ssl_certfile="./cert.pem",
ssl_ca_certs="./ca-bundle.pem", # optional: client cert verification
ssl_ciphers="TLSv1.2", # optional: minimum TLS version
)
ℹ️
Production recommendation: Terminate TLS at Nginx or a load balancer, not at Uvicorn. Let Nginx handle certificate management (certbot/Let's Encrypt), HTTP→HTTPS redirects, and HTTP/2. Uvicorn talks plain HTTP on localhost behind the proxy. This is simpler and gives you more control.
For production, use Gunicorn as the process manager with UvicornWorker as the worker class. Gunicorn handles worker lifecycle, graceful restarts, and health monitoring. Uvicorn handles the actual async I/O within each worker process.
pip install gunicorn "uvicorn[standard]"
# Basic — 4 workers on port 8000
gunicorn main:app \
--workers 4 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000
# With Unix socket (recommended with Nginx)
gunicorn main:app \
--workers 4 \
--worker-class uvicorn.workers.UvicornWorker \
--bind unix:/run/uvicorn.sock \
--umask 007 \
--daemon \
--pid /run/gunicorn.pid
# For HTTP/2 support
gunicorn main:app \
--worker-class uvicorn.workers.UvicornH2CWorker \
--workers 4
gunicorn.conf.py — recommended approach
import multiprocessing
import os
# Binding
bind = f"0.0.0.0:{os.getenv('PORT', '8000')}"
backlog = 2048
# Worker processes
workers = 2 * multiprocessing.cpu_count() + 1
worker_class = "uvicorn.workers.UvicornWorker"
worker_connections = 1000
# Restart workers to prevent memory leaks
max_requests = 10000
max_requests_jitter = 1000 # randomise to avoid thundering herd
# Timeouts
timeout = 120
keepalive = 5
graceful_timeout = 30
# Logging
accesslog = "-" # stdout
errorlog = "-" # stdout
loglevel = "info"
access_log_format = '%(h)s "%(r)s" %(s)s %(b)s %(D)sµs'
# Process naming
proc_name = "myapp-gunicorn"
gunicorn main:app --config gunicorn.conf.py
Single-stage (simple)
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
Multi-stage (production, smaller image)
# Build stage
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --prefix=/install --no-cache-dir -r requirements.txt
# Production stage
FROM python:3.12-slim AS production
WORKDIR /app
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Copy dependencies from builder
COPY --from=builder /install /usr/local
# Copy application code
COPY --chown=appuser:appuser . .
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
CMD ["gunicorn", "main:app", \
"--worker-class", "uvicorn.workers.UvicornWorker", \
"--workers", "4", \
"--bind", "0.0.0.0:8000", \
"--timeout", "120", \
"--access-logfile", "-", \
"--error-logfile", "-"]
docker build -t myapp:latest .
docker run -d -p 8000:8000 --name myapp myapp:latest
# With environment variables
docker run -d -p 8000:8000 \
-e DATABASE_URL=postgresql://... \
-e SECRET_KEY=... \
myapp:latest
upstream uvicorn {
server unix:/run/uvicorn.sock fail_timeout=0;
# Or TCP: server 127.0.0.1:8000 fail_timeout=0;
}
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_session_cache shared:SSL:10m;
# Security headers
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=31536000" always;
# Static files served directly by Nginx
location /static/ {
alias /app/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
# WebSocket upgrade headers
location /ws/ {
proxy_pass http://uvicorn;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 86400; # long-lived WS connections
}
# All other requests → Uvicorn
location / {
proxy_pass http://uvicorn;
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;
proxy_redirect off;
proxy_buffering off; # disable for streaming responses / SSE
}
}
⚠️
Forwarded headers: When behind Nginx, your app sees 127.0.0.1 as the client IP. Use the X-Forwarded-For and X-Forwarded-Proto headers to get the real client IP and scheme. In FastAPI/Starlette, add ProxyHeadersMiddleware to trust these headers. Only enable this if you are behind a trusted proxy.
{
"version": 1,
"disable_existing_loggers": false,
"formatters": {
"default": {
"format": "%(asctime)s %(levelname)s %(name)s %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S"
},
"access": {
"format": "%(asctime)s %(levelname)s [%(name)s] %(client_addr)s - \"%(request_line)s\" %(status_code)s"
}
},
"handlers": {
"default": { "class": "logging.StreamHandler", "formatter": "default", "stream": "ext://sys.stderr" },
"access": { "class": "logging.StreamHandler", "formatter": "access", "stream": "ext://sys.stdout" }
},
"loggers": {
"uvicorn": { "handlers": ["default"], "level": "INFO", "propagate": false },
"uvicorn.error": { "level": "INFO" },
"uvicorn.access": { "handlers": ["access"], "level": "INFO", "propagate": false }
}
}
uvicorn main:app --log-config log_config.json
Structured JSON logging (with python-json-logger)
pip install python-json-logger
# In your app's logging setup:
import logging
from pythonjsonlogger import jsonlogger
logger = logging.getLogger()
handler = logging.StreamHandler()
formatter = jsonlogger.JsonFormatter(
fmt='%(asctime)s %(levelname)s %(name)s %(message)s'
)
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
| Feature |
Uvicorn |
Gunicorn |
Hypercorn |
Daphne |
uWSGI |
| Protocol |
ASGI |
WSGI (+ ASGI via worker) |
ASGI |
ASGI |
WSGI |
| HTTP/2 |
✓ (via h2) |
✗ |
✓ native |
~ partial |
✗ |
| WebSockets |
✓ |
✗ |
✓ |
✓ |
✗ |
| Process management |
~ basic |
✓ excellent |
~ basic |
~ basic |
✓ excellent |
| Hot reload (dev) |
✓ |
✗ |
✓ |
✗ |
✗ |
| Performance |
⭐⭐⭐⭐⭐ |
⭐⭐⭐ |
⭐⭐⭐⭐ |
⭐⭐⭐ |
⭐⭐⭐ |
| Production maturity |
⭐⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
⭐⭐⭐ |
⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
| Best for |
FastAPI / Starlette / ASGI apps |
WSGI apps + Uvicorn workers |
HTTP/2-heavy apps |
Django Channels |
Legacy Python apps |
✅
Production recommendation: Use Gunicorn + UvicornWorker rather than running Uvicorn directly with --workers. Gunicorn provides superior process management, graceful restarts, and health monitoring that Uvicorn's own multi-process mode lacks.
import asyncio
import signal
import uvicorn
from uvicorn.config import Config
from uvicorn.main import Server
# ── Simple run ──
uvicorn.run("main:app", host="0.0.0.0", port=8000)
# ── Advanced: run alongside other async tasks ──
async def serve():
config = Config("main:app", host="0.0.0.0", port=8000, log_level="info")
server = Server(config)
await server.serve()
# ── Run server + background task concurrently ──
async def background_worker():
while True:
await asyncio.sleep(60)
# do something every minute
async def main():
config = Config("main:app", host="0.0.0.0", port=8000)
server = Server(config)
async with asyncio.TaskGroup() as tg:
tg.create_task(server.serve())
tg.create_task(background_worker())
asyncio.run(main())
# ── Graceful shutdown from inside the app ──
async def run_with_shutdown():
config = Config("main:app", host="0.0.0.0", port=8000)
server = Server(config)
loop = asyncio.get_event_loop()
def shutdown():
server.should_exit = True # trigger graceful shutdown
loop.add_signal_handler(signal.SIGTERM, shutdown)
await server.serve()
Key uvicorn.run() parameters
| Parameter | Type | Default | Description |
| app | str | callable | — | ASGI app or module:attribute string |
| host | str | "127.0.0.1" | Bind host |
| port | int | 8000 | Bind port |
| uds | str | None | Unix domain socket path |
| reload | bool | False | Enable auto-reload (app must be string) |
| reload_dirs | list[str] | None | Directories to watch for reload |
| workers | int | None | Number of worker processes |
| log_level | str | "info" | Logging level |
| access_log | bool | True | Enable access logging |
| use_colors | bool | None | Colorised output (auto-detected) |
| proxy_headers | bool | True | Trust X-Forwarded-* headers |
| forwarded_allow_ips | str | "127.0.0.1" | IPs to trust for proxy headers |
| ssl_keyfile | str | None | Path to SSL private key |
| ssl_certfile | str | None | Path to SSL certificate |
| timeout_keep_alive | int | 5 | Keep-alive timeout in seconds |
| limit_max_requests | int | None | Worker restarts after N requests |
| limit_concurrency | int | None | Max concurrent connections |
Development
# Auto-reload on file change
uvicorn main:app --reload
# Custom port
uvicorn main:app --reload --port 3000
# Watch extra dirs (e.g. templates)
uvicorn main:app --reload \
--reload-dir src --reload-dir templates
# Debug log level
uvicorn main:app --reload --log-level debug
Production (direct)
# Multi-worker on all interfaces
uvicorn main:app --host 0.0.0.0 \
--workers 4
# Unix socket (Nginx-friendly)
uvicorn main:app \
--uds /tmp/uvicorn.sock
# Limit connections, recycle workers
uvicorn main:app --workers 4 \
--limit-max-requests 10000 \
--no-access-log
Gunicorn + Uvicorn
# Basic
gunicorn main:app \
-w 4 \
-k uvicorn.workers.UvicornWorker
# With config file
gunicorn main:app \
-c gunicorn.conf.py
# Reload workers gracefully
kill -HUP $(cat /run/gunicorn.pid)
Programmatic
import uvicorn
# Simplest
uvicorn.run("main:app", reload=True)
# Production config
uvicorn.run(
"main:app",
host="0.0.0.0",
port=8000,
workers=4,
log_level="info",
)
SSL
# Generate self-signed cert
openssl req -x509 -newkey rsa:4096 \
-keyout key.pem -out cert.pem \
-days 365 -nodes -subj "/CN=localhost"
# Run with SSL
uvicorn main:app \
--ssl-keyfile key.pem \
--ssl-certfile cert.pem \
--port 443
Environment vars
# Uvicorn reads these env vars
UVICORN_HOST=0.0.0.0
UVICORN_PORT=8000
UVICORN_WORKERS=4
UVICORN_LOG_LEVEL=info
UVICORN_RELOAD=false
UVICORN_ACCESS_LOG=true
# Then just:
uvicorn main:app
📚
Further reading: Official docs at www.uvicorn.org. ASGI spec at asgi.readthedocs.io. For FastAPI-specific deployment patterns see fastapi.tiangolo.com/deployment. For benchmarks, check www.techempower.com/benchmarks (Python section).