Deploy Django with Granian
Run Django in production with Granian, the Rust HTTP server for Python: WSGI and ASGI interfaces, workers and threads, a systemd unit, Nginx in front, and when to choose it.
Problem statement
Gunicorn has been the default Django application server for over a decade, and Uvicorn (often under Gunicorn) is the usual choice for ASGI. Granian is a newer server written in Rust that speaks WSGI, ASGI, and its own RSGI interface from a single binary. It handles HTTP parsing and connection management in Rust, supports HTTP/2, and does not need a separate process manager for its workers.
Switching servers is easy to do badly: picking the wrong interface, copying Gunicorn worker counts that do not translate, or losing features you relied on, such as graceful reloads or request timeouts. This guide deploys Django with Granian correctly and explains when it is worth choosing.
pip install granianinto the app's virtual environment.- For a synchronous Django app, run
granian --interface wsgi config.wsgi:application. - For an async app, run
granian --interface asgi config.asgi:application. - Set
--workers(processes) and, for WSGI,--blocking-threads(threads per worker) based on load tests. - Bind to
127.0.0.1:8000and keep Nginx in front for TLS, static files, buffering, and limits. - Run it under systemd with
Restart=on-failure, and load test before switching production traffic.
Step-by-step solution
1. Install
/srv/myapp/venv/bin/pip install granian
/srv/myapp/venv/bin/granian --version
/srv/myapp/venv/bin/granian --help
Granian ships prebuilt wheels for common Linux platforms. Options have evolved between releases, so check --help for the version you installed and pin it in your requirements file.
2. Choose the interface
| Your Django app | Interface | Target |
|---|---|---|
| Synchronous views, standard ORM usage | wsgi | config.wsgi:application |
| Async views, Channels-free streaming, long-lived responses | asgi | config.asgi:application |
WSGI is the safer default. Django's ORM is synchronous, and under ASGI every synchronous call is run in a thread through sync_to_async, which adds overhead for an app that is mostly synchronous. Choose ASGI when you have genuinely async code paths.
3. Run it manually first
cd /srv/myapp/app
set -a; . /etc/myapp.env; set +a
/srv/myapp/venv/bin/granian --interface wsgi \
--host 127.0.0.1 --port 8000 \
--workers 2 --blocking-threads 4 \
config.wsgi:application
In another shell:
curl -sI -H "Host: example.com" http://127.0.0.1:8000/ | head -1
4. Size workers and threads
--workers: number of processes. Each holds a full copy of Django, so memory scales with this number. Start with the number of CPU cores.--blocking-threads: for WSGI, the number of threads per worker that run Django code. Views that wait on the database or external APIs benefit from a few threads; CPU-bound views do not.
Gunicorn's classic 2 x cores + 1 rule was designed for single-threaded sync workers. With Granian, start with workers = cores and a small thread count, then load test with realistic traffic and watch latency percentiles, CPU, and memory.
5. Create a systemd unit
# /etc/systemd/system/myapp.service
[Unit]
Description=myapp (Granian)
After=network.target
[Service]
User=myapp
Group=myapp
WorkingDirectory=/srv/myapp/app
EnvironmentFile=/etc/myapp.env
ExecStart=/srv/myapp/venv/bin/granian --interface wsgi \
--host 127.0.0.1 --port 8000 \
--workers 2 --blocking-threads 4 \
--respawn-failed-workers \
--access-log \
config.wsgi:application
Restart=on-failure
KillSignal=SIGINT
TimeoutStopSec=30
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
journalctl -u myapp -f
--respawn-failed-workers restarts a worker process that crashes instead of leaving the server with fewer workers. Granian also offers options to recycle workers after a lifetime, which can contain slow memory growth in the same way as Gunicorn's --max-requests; see --help for the names in your version.
6. Keep Nginx in front
upstream myapp {
server 127.0.0.1:8000;
keepalive 16;
}
server {
listen 443 ssl;
server_name example.com;
# ssl_certificate lines from Certbot
client_max_body_size 20m;
location /static/ {
alias /srv/myapp/app/staticfiles/;
expires 30d;
}
location / {
proxy_pass http://myapp;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
}
}
Nginx still does useful work Granian does not replace: TLS with Certbot renewal, efficient static file serving, request body limits, buffering slow clients, and rate limiting. Upstream keepalive avoids opening a new TCP connection per request.
Granian does not enforce a per-request timeout the way Gunicorn's --timeout kills stuck workers. Use Nginx's proxy_read_timeout, database statement_timeout, and client timeouts on outbound HTTP calls so a slow request cannot hold a thread forever.
7. Deploy updates
cd /srv/myapp/app && git pull
/srv/myapp/venv/bin/pip install -r requirements.txt
/srv/myapp/venv/bin/python manage.py migrate --noinput
/srv/myapp/venv/bin/python manage.py collectstatic --noinput
sudo systemctl restart myapp
A restart briefly refuses connections while the new process starts. Nginx can retry on another upstream if you run two instances on different ports and restart them one at a time.
Verification check:
systemctl is-active myapp
curl -sI https://example.com/ | head -1
ps -o pid,rss,cmd -u myapp | grep granian
Compare memory per worker and latency with your previous server under the same load test.
Explanation
Granian runs the network layer (accepting connections, parsing HTTP, managing keep-alive and HTTP/2) in Rust threads and hands complete requests to Python workers through the interface you select. For WSGI, each worker runs your Django application in a pool of blocking threads. The operational model is the same as Gunicorn's: a supervisor process manages workers, systemd manages the supervisor, and Nginx faces the internet. What changes is throughput per worker, memory profile, and the available options.
Edge cases / notes
When to stay on Gunicorn
Gunicorn has a larger ecosystem of documentation, integrations, and operational knowledge, plus features such as --timeout worker killing and graceful HUP reloads. If your bottleneck is the database or external APIs, changing the application server rarely matters. Switch to Granian when load tests show a real gain for your workload.
WebSockets
Granian's ASGI interface supports WebSockets. For Django Channels, run the Channels ASGI application under the asgi interface and configure Nginx to pass Upgrade headers.
Rollback
Keep the Gunicorn unit file in the repository. Rolling back is swapping ExecStart, daemon-reload, and restart.
When to automate this
Put the server command and its options in one place (a systemd template or container CMD) managed by your deploy tooling, so switching servers or changing worker counts is a reviewed change rather than an edit on the server.
Next steps
- Decide on the interface with Django WSGI vs ASGI: Which One Should You Deploy?.
- Compare with the common ASGI setup in Deploy Django ASGI with Uvicorn and Nginx.
- Apply the same sizing thinking from How to Optimize Gunicorn Workers for Django.
- Measure before switching with How to Load Test a Django App Before Release.
FAQ
Is Granian faster than Gunicorn for Django?
Often in synthetic benchmarks, because HTTP handling runs in Rust. For typical database-bound Django apps, the difference is smaller. Load test your own app before deciding.
Should Django use Granian's WSGI or ASGI interface?
Use WSGI for synchronous apps, which is most Django projects. Use ASGI when you have async views or long-lived connections that benefit from it.
Do I still need Nginx with Granian?
For most deployments, yes. Nginx handles TLS certificates, static files, upload limits, buffering, and rate limiting in front of the application server.
Does Granian replace Uvicorn for Django Channels?
It can run the Channels ASGI application with the asgi interface, including WebSockets. Test your consumers under load before switching.