Performance Research on Different Django Servers and Deployment Methods
WSGI is top-ranked? uWSGI is the real deal. ASGI adds complexity and performs significantly worse. Tencent Cloud servers even suffer from downtime? Enough said.
目录19 个章节
Preface
Recently, I've spent a lot of time tinkering with DjangoStarter.
Last time, I mentioned switching the server to Granian, developed in Rust.
Although this allowed me to remove the Nginx container, making things more convenient, the performance was still not entirely satisfactory.
So, I decided to try out most of the available servers one by one while optimizing the code.
After these past few days, investing a significant amount of time in environment configuration, code modifications, and performance testing, I've finally summarized the performance of the DjangoStarter framework with different servers.
PS: The current popularity of AI has rendered most introductory technical articles less meaningful. Nowadays, I mostly write articles as notes and try to avoid writing overly basic introductions, as they don't add much value. 😅
The less appealing parts for most readers, like code and detailed data, are placed below.
Test Environment
The project for this test was deployed on a Tencent Cloud server with 2 cores and 2GB RAM (a small, low-performance server). It feels like the server's performance seriously drags down the application's performance, haha 🤣
I also noticed something: Tencent Cloud seems to secretly reduce server performance during peak hours (like afternoons). The results from afternoon tests differed significantly from those during early morning hours.
The WSGI/ASGI servers participating in this test include daphne, granian, gunicorn, uwsgi, uvicorn, and hypercorn, which basically cover most of the servers in the Python ecosystem.
This time, wrk was used for performance testing. All interfaces were tested with the parameters -t4 -c200 -d30s.
Performance Test Conclusions
RPS Rankings
| Rank | Server | Req/sec | Interface |
|---|---|---|---|
| 🥇 1 | uWSGI + WSGI | 1206 | WSGI |
| 🥈 2 | Gunicorn + WSGI | 740 | WSGI |
| 🥉 3 | Granian + ASGI | 220 | ASGI |
| 4 | Daphne + ASGI | 190 | ASGI |
| 5 | Uvicorn + ASGI | 181 | ASGI |
| 6 | Hypercorn + ASGI | 168 | ASGI |
Conclusion:
-
WSGI is significantly faster overall than ASGI
This is an expected result, as Django's internal architecture is natively WSGI, and synchronous routing does not require additional async conversion.
-
uWSGI once again proves itself to be the king of WSGI performance
uWSGI's C implementation, mature worker management, and optimized memory utilization make it unbeatable in pure WSGI scenarios.
Memory Usage Comparison
| Rank | server | Memory |
|---|---|---|
| 🥇 1 | uWSGI | 58M (extremely low) |
| 2 | Granian | 170M |
| 3 | Daphne | 175M |
| 4 | Hypercorn | 300M |
| 5 | Gunicorn+WSGI | 430M |
| 6 | Uvicorn | 500M (highest) |
Conclusion:
- uWSGI is not only the fastest but also the most memory-efficient.
- Uvicorn has the highest memory usage (the overhead of a single Python worker is significant).
- It is normal for ASGI servers to generally have higher memory usage.
Performance Test Data
Below are the detailed performance test data.
daphne + asgi
Memory peak usage around 175M during testing.
$ wrk -t4 -c200 -d30s http://127.0.0.1:9875/api/django-starter/monitoring/health
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.03s 134.59ms 1.99s 89.71%
Req/Sec 69.17 64.53 313.00 80.49%
5737 requests in 30.04s, 2.14MB read
Socket errors: connect 0, read 0, write 0, timeout 4
Requests/sec: 190.99
Transfer/sec: 72.93KB
granian + asgi
During testing, the peak memory usage is approximately 170M.
$ curl http://127.0.0wrk -t4 -c200 -d30s875/api/django-starter/monitoring/health
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 886.48ms 79.27ms 1.26s 85.40%
Req/Sec 86.07 112.58 490.00 84.05%
6637 requests in 30.04s, 2.72MB read
Requests/sec: 220.93
Transfer/sec: 92.56KB
gunicorn + wsgi
Peak memory usage during testing was around 430MB.
$ wrk -t4 -c200 -d30s http://127.0.0.1:9875/api/django-starter/monitoring/health
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 268.67ms 31.82ms 425.23ms 86.13%
Req/Sec 187.47 119.54 494.00 56.66%
22244 requests in 30.03s, 9.52MB read
Requests/sec: 740.60
Transfer/sec: 324.74KB
uwsgi + wsgi
During testing, peak memory usage was approximately 58MB.
$ wrk -t4 -c200 -d30s http://127.0.0.1:9875/api/django-starter/monitoring/health
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 199.52ms 277.85ms 1.97s 87.56%
Req/Sec 302.89 88.84 575.00 67.33%
36210 requests in 30.02s, 12.95MB read
Socket errors: connect 0, read 36216, write 0, timeout 96
Requests/sec: 1206.08
Transfer/sec: 441.68KB
uvicorn + asgi
During testing, peak memory usage was around 500M.
$ wrk -t4 -c200 -d30s http://127.0.0.1:9875/api/django-starter/monitoring/health
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.00s 537.23ms 2.00s 59.81%
Req/Sec 62.04 66.05 460.00 83.03%
5440 requests in 30.04s, 2.23MB read
Socket errors: connect 0, read 0, write 0, timeout 369
Requests/sec: 181.07
Transfer/sec: 76.05KB
hypercorn + asgi
During testing, the peak memory usage was around 300MB.
$ wrk -t4 -c200 -d30s http://127.0.0.1:9875/api/django-starter/monitoring/health
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 734.64ms 423.05ms 2.00s 51.70%
Req/Sec 55.53 52.50 360.00 83.49%
5064 requests in 30.06s, 2.09MB read
Socket errors: connect 0, read 0, write 0, timeout 1124
Requests/sec: 168.45
Transfer/sec: 71.24KB
More Refined Performance Test
Accessing an interface with database query: http://127.0.0.1:9878/api/demo/movie/movie
Direct uwsgi
$ wrk -t4 -c200 -d30s http://127.0.0.1:9875/api/demo/movie/movie
Running 30s test @ http://127.0.0.1:9875/api/demo/movie/movie
4 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 501.93ms 260.82ms 1.99s 94.20%
Req/Sec 61.02 39.11 191.00 62.02%
6726 requests in 30.04s, 15.84MB read
Socket errors: connect 0, read 6758, write 0, timeout 45
Requests/sec: 223.89
Transfer/sec: 539.82KB
After caddy reverse proxy
$ wrk -t4 -c200 -d30s http://127.0.0.1:9878/api/demo/movie/movie
Running 30s test @ http://127.0.0.1:9878/api/demo/movie/movie
4 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 562.46ms 166.56ms 1.65s 88.77%
Req/Sec 52.66 21.07 150.00 65.63%
6299 requests in 30.04s, 15.06MB read
Socket errors: connect 0, read 0, write 0, timeout 208
Non-2xx or 3xx responses: 40
Requests/sec: 209.69
Transfer/sec: 513.30KB
Code
Let's introduce the code section.
The main items used for performance testing this time are the health check and a simple database access API. All APIs are developed using django-ninja.
Database Access Interface
This is the interface involved in database operations.
Query data return with pagination functionality, very straightforward.
@router.get('/movie', response=List[MovieOut], url_name='demo/movie/list')
@paginate
def list_items(request):
qs = Movie.objects.all()
return qs
Health Check Interface
Import these packages
import asyncio
from ninja import Router
from django.db import connections
from django.db.utils import OperationalError
from redis import Redis
from redis.exceptions import RedisError
from redis import asyncio as aioredis
import os
import time
import platform
import subprocess
import anyio
The original interface was purely synchronous, and it also called a system process to obtain uptime, which severely slowed down the entire interface. This time, I have refactored it into both synchronous and asynchronous interfaces.
First is the simplest version, which does nothing and simply returns JSON.
@router.get('health')
def simple_health_check(request):
"""Health check endpoint for container health checks and monitoring"""
response_data = {
'status': 'healthy',
'status_code': 200,
}
return response_data
Asynchronous Interface
Let's start with the asynchronous interface.
Actually, I rarely use Python's asynchronous features, which might be related to Python's relatively weak support for asynchronicity.
First, let's write two asynchronous methods to check database and Redis connections.
async def check_db_async():
"""Database check (synchronous ORM → run in thread pool)"""
try:
def _check():
for conn in connections.all():
conn.cursor()
return True
return await anyio.to_thread.run_sync(_check)
except OperationalError:
return False
async def check_redis_async():
"""Asynchronous Redis check (Note: Ensure client is closed to release connection pool)"""
try:
redis_client = aioredis.Redis(
host="redis",
port=6379,
socket_connect_timeout=1,
decode_responses=True,
)
try:
ok = await redis_client.ping()
return bool(ok)
finally:
# Close client and connection pool to avoid connection leaks (important in short-lived containers/requests)
try:
await redis_client.close()
except Exception:
pass
try:
await redis_client.connection_pool.disconnect()
except Exception:
pass
except Exception:
return False
Django ORM does not support asynchronous operations, so here we use the anyio package to simplify things by wrapping synchronous operations to run asynchronously. You could also do this with the asyncio library, but anyio is more convenient.
When using aioredis, there's a pitfall I encountered. I initially installed the aioredis package, only to find it conflicted with the existing redis package.
After checking the documentation, I realized that from redis import asyncio as aioredis is all you need—the redis package already includes built-in asynchronous support.
Now, let's implement the asynchronous interface.
@router.get('health/async')
async def health_check_async(request):
"""Async mode health check (ASGI optimized version)"""
# Use asyncio.gather to concurrently run DB and Redis checks
db_ok, redis_ok = await asyncio.gather(
check_db_async(),
check_redis_async(),
)
status = "healthy" if db_ok and redis_ok else "unhealthy"
status_code = 200 if status == "healthy" else 503
return {
"status": status,
"status_code": status_code,
"checks": {
"database": "ok" if db_ok else "error",
"redis": "ok" if redis_ok else "error",
},
"system": get_system_info(),
}
Synchronous Interface
It's simpler with synchronous
@router.get('health/sync')
def health_check_sync(request):
"""Synchronous version of the health check interface"""
# --- Check Database & Redis ---
db_ok = check_db_sync()
redis_ok = check_redis_sync()
# --- System Information ---
system_info = {
"timestamp": time.time(),
"uptime": get_uptime(),
"hostname": os.environ.get("HOSTNAME", ""),
"environment": os.environ.get("ENVIRONMENT", "development"),
"os": platform.system(),
}
# --- Status Code ---
status = "healthy" if db_ok and redis_ok else "unhealthy"
status_code = 200 if status == "healthy" else 503
# --- Return Data ---
return {
"status": status,
"status_code": status_code,
"checks": {
"database": "ok" if db_ok else "error",
"redis": "ok" if redis_ok else "error",
},
"system": system_info,
}
Summary
For traditional applications, uWSGI is the straightforward choice.
For those needing asynchronous capabilities, you can split out the async parts and run them using granian or daphne.
Of course, this also increases complexity, but it's the optimal trade-off for performance optimization.
That's it for now. Actually, this performance investigation taught me a lot, and I'll continue analyzing my takeaways in the next article.
_EOF_
Performance Research on Different Django Servers and Deployment Methods
- 本文作者:
- 程序设计实验室
- 版权声明:
- 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
相关文章
Django中「强行」使用MVVM设计模式
扫盲 首先带大家了解一下什么是MVVM模式: 什么是MVVM?MVVM是Model-View-ViewModel的缩写。 MVVM是MVC的增强版,实质上和MVC没有本质区别,只是代码的位置变动而已 从名字上看,MVVM比MVC架构中多了一个ViewModel,没错,就是这个ViewModel,他是MVVM相对于MVC改进的核心思想。在开发过程中,由于需求的变更或添加,项目的复杂度越来越高,代码量
2021-12-18 · Django · 约 1 分钟
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:为模板渲染加入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 分钟
讨论
共 0 条加载评论中…