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官方实现更好的分页组件+Bootstrap整合
前言 Django全家桶自带的分页组件只能说能满足分页这个功能,但是没那么好用就是了 Django的分页效果 django-pure-pagination分页效果 使用方法 首先安装: pip install django-pure-pagination 添加到App INSTALLED_APPS = ( ... 'pure_pagination', ) 配置一下,配置说明如图 P
2021-12-18 · Django · 约 1 分钟
Django-Import-Export插件关于外键的处理
前言 Django-Import-Export是一款很好用很方便的Django数据导出导入插件,可以和DjangoAdmin管理后台完美集成,只需要少量的代码配置即可方便实现你要的多种格式导出导入,关于这个插件的使用更多可以看我之前的文章:这里放文章链接 之前我在使用中都是专门做了一个原始数据的表来存导入的数据,然后再对原始数据表做一些数据处理,把数据存到其他表才能真正使用这些数据。(不是很好的做
2022-04-02 · Django · 约 1 分钟
聊聊Django应用的部署和性能的那些事儿
前言 随着工作的深入,我越来越发现Python Web开发中有很多坑,也一直在羡慕AspNetCore和Go等的可执行文件部署和高性能,以及Spring生态的丰富,不过因为工作用了Django,生活还是要继续的嘛,这Django好歹也是有很大份额的Web框架,也没那么不堪,至少开发速度上就吊打一众框架了~ 在之前的文章里我介绍过使用Docker部署Django应用的方法,不过那种部署方式只适合上线
2022-05-27 · Django · 约 1 分钟
Django-Filter介绍与刁钻需求的实现方法
Django-Filter是一个非常好用的第三方库,很好的利用了Django ORM的特性,可以使用很少的代码就扩展原有的接口,实现多种筛选功能~ 场景 Model定义的部分代码,可以看到需求模型包括了 关键词 keyword、区域 region、需求状态 require_status,这三个字段 class Require(models.Model): 需求 keyword =
2021-12-18 · Django · 约 1 分钟
讨论
共 0 条加载评论中…