A Guide to Granian in Python: A Rust-Based ASGI Web Server to Replace Uvicorn

Python tutorial - IT technology blog
Python tutorial - IT technology blog

Background: When Uvicorn Starts to Fall Behind

About six months ago, our Core API cluster constantly faced overloads between 11:00 AM and 1:00 PM. Our stack was standard at the time: FastAPI behind Uvicorn workers, managed by Gunicorn on Kubernetes. When traffic spiked to 12,000 requests per second, the system started to buckle. Pod CPU usage shot up to 90-95%, while p99 latency surged from 35ms all the way to 420ms.

The bottleneck wasn’t in our database or queries; it was right in Python’s HTTP server runtime layer. Even with FastAPI and uvloop heavily optimized, Python still bears the burden of HTTP parsing, socket management, and event loop scheduling. As concurrent connections grow, Python’s GIL and runtime overhead become glaring weaknesses.

That is why we experimented with Granian and decided to deploy it straight to production.

Granian is a Python HTTP server written entirely in Rust, built on two renowned networking libraries: hyper and tokio. This architecture completely offloads the I/O burden from Python. TCP connection handling, HTTP/1 and HTTP/2 parsing, and TLS termination are all handled natively by Rust. The Python app simply receives the pre-parsed request payload to execute its core business logic.

The results were immediately apparent. On the exact same pod spec of 2 vCPUs and 2GB RAM, switching to Granian doubled our application’s throughput. p99 latency dropped below 80ms, while RAM consumption remained remarkably stable.

Installing Granian

Granian comes as pre-compiled binary wheels on PyPI. You can start using it right away without needing cargo or rustc installed on your server.

# Install via pip
pip install granian

# Use Poetry
poetry add granian

# Use uv (recommended for ultra-fast wheel downloads)
uv pip install granian

Verify the installation:

granian --version

Create a simple main.py file with FastAPI to verify the configuration:

from fastapi import FastAPI

app = FastAPI(title="Granian Performance Demo")

@app.get("/")
async def root():
    return {"status": "ok", "server": "Granian", "engine": "Rust Tokio"}

@app.get("/compute")
async def compute():
    # Simulate lightweight processing
    total = sum(i * 2 for i in range(1000))
    return {"result": total}

Production Configuration Guide

Granian supports three interfaces: ASGI (FastAPI, Starlette), WSGI (Flask, Django), and RSGI (a custom interface designed by Granian’s author to maximize throughput). For most current projects, ASGI remains the safest and most compatible choice.

1. Basic Startup

granian --interface asgi main:app --host 0.0.0.0 --port 8000

2. Optimizing Workers and Threads

Granian combines workers (isolated processes) with threads (Tokio runtime threads per worker). This offers a huge advantage over Uvicorn’s single-threaded event loop architecture:

granian --interface asgi main:app \
    --host 0.0.0.0 \
    --port 8000 \
    --workers 2 \
    --threads 2 \
    --backlog 4096 \
    --http 1 \
    --opt

Key parameters explained:

  • --workers: Number of independent worker processes. In a Kubernetes containerized environment (typically allocated 2 vCPUs), 2 is the ideal number. Avoid configuring too many workers, which introduces CPU context-switching overhead.
  • --threads: Number of Tokio threads per worker. These threads process network I/O concurrently in Rust without being blocked by Python’s GIL.
  • --backlog 4096: TCP connection queue size. This helps absorb short-term traffic spikes without dropping client connections.
  • --opt: Enables internal data conversion optimizations between Rust’s PyO3 and the Python runtime.
  • --http 1: Enforces HTTP/1.1 if your app sits behind Nginx or an Ingress Controller, reducing protocol negotiation overhead.

3. Production-Ready Dockerfile

Here is a lightweight, battle-tested Dockerfile with optimized layer caching that you can adopt immediately:

FROM python:3.11-slim as base

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=off

WORKDIR /app

# Install curl for container healthchecks
RUN apt-get update && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .

EXPOSE 8000

# Configure 2 workers, 2 threads, and disable WebSockets if not needed
CMD ["granian", "--interface", "asgi", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2", "--threads", "2", "--no-ws"]

The --no-ws flag completely disables the WebSocket subsystem. If your API only serves standard REST endpoints, this saves additional RAM and CPU cycles per connection.

Benchmarks & Real-World Evaluation

Never rely solely on online benchmarks. Always measure performance against your own infrastructure before cutting over production traffic.

1. Measuring Throughput with wrk

Run wrk from a separate node on the same internal network. Avoid running benchmarks on the web server itself, as the load generator will directly compete for CPU resources with the server:

# Run for 30s with 8 threads and 200 concurrent connections over internal IP
wrk -t8 -c200 -d30s http://10.0.1.50:8000/compute

In our benchmark testing on the lightweight /compute endpoint, Granian reached around 28,500 RPS compared to 16,200 RPS for the Gunicorn + Uvicorn worker setup (~75% increase). The most impressive takeaway was the latency distribution: Granian’s p95 and p99 curves remained remarkably flat, showing none of the sudden latency spikes caused by event loop contention.

2. Memory Monitoring

Monitor worker RAM consumption directly:

# Inspect process details
ps aux | grep granian

# Monitor resources in real time via top
top -p $(pgrep -d',' -f granian)

In production, each Granian worker stabilized at 55-70MB of RAM during idle periods. Under a 5,000 RPS load, memory usage climbed to roughly 95MB and plateaued there. There was zero evidence of memory leaks within the Rust layer.

3. Access Log Management

Writing synchronous access logs to the console under heavy load can choke container I/O. In production, you should disable default access logging and only enable it when debugging:

# Production run: set log level to warning and disable access logs for optimal I/O
granian --interface asgi main:app \
    --host 0.0.0.0 \
    --port 8000 \
    --log-level warning \
    --no-access-log

Granian is a worthwhile upgrade for modern Python ASGI systems. You gain the raw performance of Rust without having to rewrite a single line of your Python codebase.

Share: