4x Performance Boost! Using Granian as the ASGI Server for Django Projects
Granian is an ASGI server written in Rust. It's a bit faster than Daphne, but still a long way from uWSGI? Static file support is convenient, though. Good enough for development; production still depends on the situation.
目录17 个章节
Introduction
Recently I've continued working on a new version of DjangoStarter.
Previously, to enable Docker deployment, I bundled Nginx into DjangoStarter's compose configuration, but this brought configuration complexity, especially when it also had to work with the framework to implement features like URL prefixes.
Starting with version v3.2.x, I launched a subtraction plan to simplify the code and features and reduce cognitive load. This Nginx container is something I've always wanted to remove as well.
Recently I discovered Granian, an ASGI server developed in Rust. It has high performance and can also serve static files, which perfectly matches my needs, so this time I'm using it to replace the originally used Daphne.
Granian
Project homepage: https://github.com/emmett-framework/granian
Granian is a high-performance Python Web Server that supports ASGI, WSGI, and RSGI. Written in Rust, it starts fast and has strong concurrency, making it a great fit for Django / FastAPI / Starlette.
The official description is “A Rust HTTP server for Python applications built on top of Hyper/Tokio”.
Features:
- Supports ASGI 3, RSGI (Rust-Server-Gateway Interface), and WSGI interfaces.
- Supports HTTP/1 and HTTP/2 (HTTP/3 planned for the future) protocols.
- Supports serving static files directly (“Direct static files serving”).
Usage
It's very simple—no need to modify code, just change the startup command.
The startup command currently used with DjangoStarter looks like this:
granian --interface asgi --host 0.0.0.0 --port 8000 --static-path-route /static --static-path-mount ./static-dist config.asgi:application
Similar to uvicorn, granian also supports hot reload—just add a --reload flag.
Performance Testing
This round of performance testing was done with wrk.
Test Data
The following data was measured on a Tencent Cloud server with 2 cores CPU + 2G of RAM.
django 5.x + ninja + daphne
$ wrk -t4 -c200 -d30s http://127.0.0.1:9876/api/django-starter/monitoring/health
Running 30s test @ http://127.0.0.1:9876/api/django-starter/monitoring/health
4 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.97s 0.00us 1.97s 100.00%
Req/Sec 38.55 55.33 343.00 90.23%
1343 requests in 30.04s, 760.03KB read
Socket errors: connect 0, read 0, write 0, timeout 1342
Requests/sec: 44.70
Transfer/sec: 25.30KB
django 5.x + ninja + granian
$ wrk -t4 -c200 -d30s http://127.0.0.1:9875/api/django-starter/monitoring/hea
lth
Running 30s test @ http://127.0.0.1:9875/api/django-starter/monitoring/health
4 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.20s 93.05ms 1.72s 82.69%
Req/Sec 83.13 105.81 460.00 83.72%
4980 requests in 30.04s, 2.85MB read
Requests/sec: 165.76
Transfer/sec: 97.31KB
Out of curiosity, I also found a much older project deployed with WSGI for comparison.
The following data was measured on a private-cloud server with 4 cores CPU + 2G of RAM.
Because the server hardware is completely different, the data is for reference only.
Django 3.x + drf + uwsgi + nginx
$ wrk -t4 -c200 -d30s http://127.0.0.1:9001/api/health/
Running 30s test @ http://127.0.0.1:9001/api/health/
4 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 295.30ms 373.95ms 1.96s 85.14%
Req/Sec 205.54 68.42 380.00 73.73%
19583 requests in 30.09s, 5.83MB read
Socket errors: connect 0, read 0, write 0, timeout 474
Non-2xx or 3xx responses: 12
Requests/sec: 650.89
Transfer/sec: 198.32KB
Django 3.x + ninja + uwsgi + nginx
$ wrk -t4 -c200 -d30s http://127.0.0.1:9001/api2/health
Running 30s test @ http://127.0.0.1:9001/api2/health
4 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 203.75ms 280.30ms 1.95s 88.44%
Req/Sec 250.81 108.59 500.00 61.21%
22542 requests in 30.06s, 5.68MB read
Socket errors: connect 0, read 0, write 0, timeout 420
Non-2xx or 3xx responses: 12
Requests/sec: 749.88
Transfer/sec: 193.35KB
Conclusion
The following conclusions come from having AI analyze the test data above. However, ASGI and uWSGI were not tested on the same server, so a direct comparison is actually quite difficult. For now, it appears that switching to Granian can indeed improve performance by 4x.
① Daphne's performance under Django + ASGI is very poor
- Latency shot straight up to the 1.9-second range,
- wrk at 200 concurrency almost flattened it,
- 1342 out of 1343 requests timed out, meaning it simply couldn't hold up.
In plain terms: Daphne is more like a development-environment server and is not recommended for high-concurrency production.
② Granian is 3–4 times faster than Daphne, but still limited
- Processes 165 req/s (same data center, same code).
- Latency is still around 1.2s, far from ideal (still struggling under high concurrency).
- The advantage is native ASGI + a Rust implementation, far stronger than Daphne.
The intuitive take: Granian is usable, but don't expect it to carry traffic the way uWSGI does.
③ uWSGI (WSGI) completely dominates: a single machine can reach the 650–750 req/s range
- Performance is a straight 4–5 times that of granian.
- Although it's an older architecture (WSGI), it's maturely tuned, stable, and has a strong allocation mechanism, so its ability to handle load is far stronger than comparable ASGI servers.
In short: If you don't need async, WSGI is still Django's strongest deployment approach (from a performance standpoint).
docker-compose
This is the slimmed-down compose configuration
services:
app:
image: ${APP_IMAGE_NAME}:${APP_IMAGE_TAG}
container_name: $APP_NAME-app
command:
- granian
- --interface
- asgi
- --host
- 0.0.0.0
- --port
- "${APP_INTERNAL_PORT:-8000}"
- --static-path-route
- /static
- --static-path-mount
- /project/static-dist
- config.asgi:application
Command-Line Arguments
Its command-line structure is very simple:
granian [OPTIONS] APP
Where:
- APP is the entry point, e.g.
config.asgi:application - OPTIONS are the various configuration parameters
Below, all parameters are organized by category (with explanations and suggestions).
🧩 Basic Parameters (required for startup)
| Parameter | Description | Default |
|---|---|---|
APP | Application entry point to start (e.g. mysite.asgi:application) | Required |
--interface | Interface type: asgi / asginl / rsgi / wsgi | rsgi |
--host | Listen address | 127.0.0.1 |
--port | Port | 8000 |
--uds | Use Unix Domain Socket | None |
--http | HTTP version: 1, 2, auto | auto |
--workers | Number of worker processes | 1 |
👉 Django and FastAPI users typically write:
--interface asgi
🗂️ Static File Serving (Django-specific configuration)
Granian has built-in static file serving:
| Parameter | Description | Default |
|---|---|---|
--static-path-route | URL route prefix, e.g. /static | /static |
--static-path-mount | File directory, e.g. /project/static-dist | None |
--static-path-expires | Cache duration (seconds) | 86400 |
Example:
--static-path-route /static --static-path-mount /project/static-dist
⚙️ Multiprocess, Thread, and Event Loop Options
Worker / Thread
| Parameter | Description |
|---|---|
--workers | Number of workers |
--blocking-threads | Number of blocking threads |
--runtime-threads | Number of runtime threads |
--runtime-blocking-threads | Runtime I/O blocking threads |
Recommendations:
- Around CPU × 2 workers is usually enough
- Most ASGI projects do not need to tune thread parameters
🔁 Event Loop & Runtime
| Parameter | Description |
|---|---|
--loop | Event loop: auto, asyncio, rloop, uvloop |
--task-impl | Task executor: asyncio / rust |
--runtime-mode | Single-threaded st / multi-threaded mt |
Recommendations:
- Ordinary projects: the defaults are fine
- High concurrency:
--task-impl rustperforms better
🔒 HTTP/1 and HTTP/2 Related Parameters
| Category | Common options |
|---|---|
| HTTP/1 | --http1-buffer-size, --http1-keep-alive |
| HTTP/2 | --http2-* series of parameters controlling flow control, windows, keepalive, number of streams, etc. |
Most projects do not need to adjust these; the defaults are fine.
📜 Logging
| Parameter | Description | Default |
|---|---|---|
--log / --no-log | Enable logging | enabled |
--log-level | Log level | info |
--log-config | Use a JSON config file | none |
--access-log | Enable access log | disabled |
--access-log-fmt | Access log format | none |
If you want Nginx-style access logs in production:
--access-log --access-log-fmt "%a %r %s %b"
🔁 Hot Reload (for development)
| Parameter | Description |
|---|---|
--reload | Enable auto-reload |
--reload-paths | Specify directories to watch |
--reload-ignore-* | Ignore directories, paths, and patterns |
For example:
--reload --reload-paths src/
🔐 HTTPS
| Parameter | Description |
|---|---|
--ssl-certificate | Certificate file |
--ssl-keyfile | Key file |
--ssl-ca | CA |
--ssl-client-verify | Client certificate verification |
(HTTPS is usually handled by Nginx as a reverse proxy)
🧰 Other Useful but Infrequently Changed Parameters
| Parameter | Description |
|---|---|
--working-dir | Change working directory |
--env-files | Load environment variable files |
--factory | Use when APP is a factory function |
--url-path-prefix | Application mount prefix |
--process-name | Custom process name |
--pid-file | Write a PID file |
--version | Show version |
--help | Show help |
_EOF_
4x Performance Boost! Using Granian as the ASGI Server for Django Projects
- 本文作者:
- 程序设计实验室
- 版权声明:
- 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
相关文章
Django应用部署:Docker容器化实践
在此之前,我一直用uwsgi+virtualenv+nginx方式进行应用部署,操作起来比较麻烦,而且依赖于服务器上的Python版本,服务的管理方面单纯uwsgi + pid算不上特别麻烦但总没有docker compose方便,无奈原本的服务器是32位系统的用不上docker,现在又增加了一个新服务器,所以赶紧把docker安排上~ 不得不说真的太简单太方便了,全程傻瓜式操作,也基本没像之前u
2021-12-18 · Django · 约 1 分钟
Django过时了吗?从ASGI到AI时代的思考
Django过时?技术趋势变了而已,异步支持短板在AI时代更明显。生态工程化再稳,也挡不住轻量框架和ASGI的冲击。独立开发者该考虑新选择了?
2025-09-12 · Django · 约 1 分钟
设置Django关闭Debug后的静态文件路由
Django在Debug模式关闭掉后请求静态文件时,返回404相应码,后台的请求url是"GET /static/css/404.css HTTP/1.1" 404 1217,找不到静态文件的url。通过配置静态文件的url和命令行启动来实现静态文件的获取。 URL中的配置 django的静态文件是通过``来管理的。配置静态文件的响应是通过serve()函数数显的。url.py中的配置如下: fr
2021-12-18 · Django · 约 1 分钟
Django-Filter介绍与刁钻需求的实现方法
Django-Filter是一个非常好用的第三方库,很好的利用了Django ORM的特性,可以使用很少的代码就扩展原有的接口,实现多种筛选功能~ 场景 Model定义的部分代码,可以看到需求模型包括了 关键词 keyword、区域 region、需求状态 require_status,这三个字段 class Require(models.Model): 需求 keyword =
2021-12-18 · Django · 约 1 分钟
讨论
共 0 条加载评论中…