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-Channels实现websocket通信+大模型对话
这个只是简单的demo,实际上生产还得考虑很多问题,本文就是为 channels 的应用开了个头,后续有新的研究成果会持续更新博客~
2024-08-14 · Django · 约 1 分钟
Django中间件之实现Admin后台IP白名单
前言 Django自带的Admin管理后台很方便,但是实际生产环境真的会有挺多安全问题的,在admin的安全防护这方面,我之前就研究实现了给admin加上登录验证码和限流功能,可以参考这篇文章:给Django Admin添加验证码和多次登录尝试限制 不过就在内部使用的话就好很多,因此我们可以通过Django的中间件来实现IP过滤,所以本文中涉及到两个知识点,一个是Django的中间件,另外一个是P
2022-08-26 · Django · 约 1 分钟
Django:为模板渲染加入Markdown支持
两种方法 Django 的django-markdown-deux模块 Python模块 markdown django-markdown-deux 首先需要安装: pip install django-markdown-deux 修改setting.py 把markdown-deux添加进去 INSTALLED_APPS = [ 'markdown-deux', ] 在模板里添加tags
2022-04-02 · Django · 约 1 分钟
Django的单元测试和集成测试
测试,在以前确实是经常被忽略的话题,特别是对于 Python Web 这种快速开发框架
2024-03-06 · Django · 约 1 分钟
讨论
共 0 条加载评论中…