DEV Community

Cover image for Uvicorn: The Lightning-Fast ASGI Server Powering Modern Python Apps
MEROLINE LIZLENT
MEROLINE LIZLENT

Posted on

Uvicorn: The Lightning-Fast ASGI Server Powering Modern Python Apps

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
Enter fullscreen mode Exit fullscreen mode

Installation

pip install uvicorn
pip install "uvicorn[standard]" //with high performance
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!",
    })
Enter fullscreen mode Exit fullscreen mode

Configuration

Workers

# Single worker (default)- fine for development
uvicorn app:app

# Multiple workers — for production on a single machine
uvicorn app:app --workers 4
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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,
    )
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
gunicorn app:app \
  -w 4 \
  -k uvicorn.workers.UvicornWorker \
  --bind 0.0.0.0:8000 \
  --timeout 60 \
  --keep-alive 5 \
  --access-logfile - \
  --error-logfile -
Enter fullscreen mode Exit fullscreen mode

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";
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
systemctl enable myapp
systemctl start myapp
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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}")
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Uvicorn calls these at the right moment during server start and graceful shutdown.

Performance Tips

  1. Always use uvicorn[standard] — the uvloop + httptools combo is a free 2-4x speedup
  2. Use--limit-max-requests in production to prevent memory leaks from gradual bloat
  3. Unix sockets over TCP when Nginx and Uvicorn are on the same machine — reduces overhead
  4. 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)