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快速开发实践:Drf框架和xadmin配置指北
步骤 既然是快速开发,那废话不多说,直接说步骤: 安装Djagno 安装Django Rest Framework 定义models 定义Rest framework的serializers 定义Rest framework的viewsets 配置Rest framework的router 配置管理后台admin 根据需要写template和对应的view 经过这些步骤就能得到一个具备完整的后端接
2022-09-06 · Django · 约 1 分钟
轻松搞定Django分页功能
view代码 from django.core.paginator import Paginator def announcement(request): ctx = { 'global': GlobalCtx, 'announcements': models.Announcement.objects.all(), } paginator
2022-05-27 · Django · 约 1 分钟
Django中间件之实现Admin后台IP白名单
前言 Django自带的Admin管理后台很方便,但是实际生产环境真的会有挺多安全问题的,在admin的安全防护这方面,我之前就研究实现了给admin加上登录验证码和限流功能,可以参考这篇文章:给Django Admin添加验证码和多次登录尝试限制 不过就在内部使用的话就好很多,因此我们可以通过Django的中间件来实现IP过滤,所以本文中涉及到两个知识点,一个是Django的中间件,另外一个是P
2022-08-26 · Django · 约 1 分钟
Django-Import-Export插件关于外键的处理
前言 Django-Import-Export是一款很好用很方便的Django数据导出导入插件,可以和DjangoAdmin管理后台完美集成,只需要少量的代码配置即可方便实现你要的多种格式导出导入,关于这个插件的使用更多可以看我之前的文章:这里放文章链接 之前我在使用中都是专门做了一个原始数据的表来存导入的数据,然后再对原始数据表做一些数据处理,把数据存到其他表才能真正使用这些数据。(不是很好的做
2022-04-02 · Django · 约 1 分钟
讨论
共 0 条加载评论中…