返回文章列表文章索引阅读全文
STARBLOG / 阅读手记
Django

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.

曦远 (DealiAxy)约 30 分钟
目录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)

ParameterDescriptionDefault
APPApplication entry point to start (e.g. mysite.asgi:application)Required
--interfaceInterface type: asgi / asginl / rsgi / wsgirsgi
--hostListen address127.0.0.1
--portPort8000
--udsUse Unix Domain SocketNone
--httpHTTP version: 1, 2, autoauto
--workersNumber of worker processes1

👉 Django and FastAPI users typically write:

--interface asgi

🗂️ Static File Serving (Django-specific configuration)

Granian has built-in static file serving:

ParameterDescriptionDefault
--static-path-routeURL route prefix, e.g. /static/static
--static-path-mountFile directory, e.g. /project/static-distNone
--static-path-expiresCache duration (seconds)86400

Example:

--static-path-route /static --static-path-mount /project/static-dist

⚙️ Multiprocess, Thread, and Event Loop Options

Worker / Thread

ParameterDescription
--workersNumber of workers
--blocking-threadsNumber of blocking threads
--runtime-threadsNumber of runtime threads
--runtime-blocking-threadsRuntime 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

ParameterDescription
--loopEvent loop: auto, asyncio, rloop, uvloop
--task-implTask executor: asyncio / rust
--runtime-modeSingle-threaded st / multi-threaded mt

Recommendations:

  • Ordinary projects: the defaults are fine
  • High concurrency: --task-impl rust performs better
CategoryCommon 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

ParameterDescriptionDefault
--log / --no-logEnable loggingenabled
--log-levelLog levelinfo
--log-configUse a JSON config filenone
--access-logEnable access logdisabled
--access-log-fmtAccess log formatnone

If you want Nginx-style access logs in production:

--access-log --access-log-fmt "%a %r %s %b"

🔁 Hot Reload (for development)

ParameterDescription
--reloadEnable auto-reload
--reload-pathsSpecify directories to watch
--reload-ignore-*Ignore directories, paths, and patterns

For example:

--reload --reload-paths src/

🔐 HTTPS

ParameterDescription
--ssl-certificateCertificate file
--ssl-keyfileKey file
--ssl-caCA
--ssl-client-verifyClient certificate verification

(HTTPS is usually handled by Nginx as a reverse proxy)

🧰 Other Useful but Infrequently Changed Parameters

ParameterDescription
--working-dirChange working directory
--env-filesLoad environment variable files
--factoryUse when APP is a factory function
--url-path-prefixApplication mount prefix
--process-nameCustom process name
--pid-fileWrite a PID file
--versionShow version
--helpShow help

_EOF_

4x Performance Boost! Using Granian as the ASGI Server for Django Projects

本文作者:
程序设计实验室
版权声明:
本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

相关文章

讨论

共 0 条

良言一句三冬暖,恶语伤人六月寒。

加载评论中…