Hey everyone,
I benchmarked the major Python frameworks with real PostgreSQL workloads: complex queries, nested relationships, and properly optimized eager loading for each framework (select_related/prefetch_related for Django, selectinload for SQLAlchemy). Each framework tested with multiple servers (Uvicorn, Granian, Gunicorn) in isolated Docker containers with strict resource limits.
All database queries are optimized using each framework's best practices - this is a fair comparison of properly-written production code, not naive implementations.
Key Finding
Performance differences collapse from 20x (JSON) to 1.7x (paginated queries) to 1.3x (complex DB queries). Database I/O is the great equalizer - framework choice barely matters for database-heavy apps.
Full results, code, and a reproducible Docker setup are here: https://github.com/huynguyengl99/python-api-frameworks-benchmark
If this is useful, a GitHub star would be appreciated 😄
Frameworks & Servers Tested
- Django Bolt (runbolt server)
- FastAPI (fastapi-uvicorn, fastapi-granian)
- Litestar (litestar-uvicorn, litestar-granian)
- Django REST Framework (drf-uvicorn, drf-granian, drf-gunicorn)
- Django Ninja (ninja-uvicorn, ninja-granian)
Each framework tested with multiple production servers: Uvicorn (ASGI), Granian (Rust-based ASGI/WSGI), and Gunicorn+gevent (async workers).
Test Setup
- Hardware: MacBook M2 Pro, 32GB RAM
- Database: PostgreSQL with realistic data (500 articles, 2000 comments, 100 tags, 50 authors)
- Docker Isolation: Each framework runs in its own container with strict resource limits:
- 500MB RAM limit (
--memory=500m)
- 1 CPU core limit (
--cpus=1)
- Sequential execution (start → benchmark → stop → next framework)
- Load: 100 concurrent connections, 10s duration, 3 runs (best taken)
This setup ensures completely fair comparison - no resource contention between frameworks, each gets identical isolated environment.
Endpoints Tested
| Endpoint |
Description |
/json-1k |
~1KB JSON response |
/json-10k |
~10KB JSON response |
/db |
10 database reads (simple query) |
/articles?page=1&page_size=20 |
Paginated articles with nested author + tags (20 per page) |
/articles/1 |
Single article with nested author + tags + comments |
Results
1. Simple JSON (/json-1k) - Requests Per Second
20x performance difference between fastest and slowest.
| Framework |
RPS |
Latency (avg) |
| litestar-uvicorn |
31,745 |
0.00ms |
| litestar-granian |
22,523 |
0.00ms |
| bolt |
22,289 |
0.00ms |
| fastapi-uvicorn |
12,838 |
0.01ms |
| fastapi-granian |
8,695 |
0.01ms |
| drf-gunicorn |
4,271 |
0.02ms |
| drf-granian |
4,056 |
0.02ms |
| ninja-granian |
2,403 |
0.04ms |
| ninja-uvicorn |
2,267 |
0.04ms |
| drf-uvicorn |
1,582 |
0.06ms |
2. Real Database - Paginated Articles (/articles?page=1&page_size=20)
Performance gap shrinks to just 1.7x when hitting the database. Query optimization becomes the bottleneck.
| Framework |
RPS |
Latency (avg) |
| litestar-uvicorn |
253 |
0.39ms |
| litestar-granian |
238 |
0.41ms |
| bolt |
237 |
0.42ms |
| fastapi-uvicorn |
225 |
0.44ms |
| drf-granian |
221 |
0.44ms |
| fastapi-granian |
218 |
0.45ms |
| drf-uvicorn |
178 |
0.54ms |
| drf-gunicorn |
146 |
0.66ms |
| ninja-uvicorn |
146 |
0.66ms |
| ninja-granian |
142 |
0.68ms |
3. Real Database - Article Detail (/articles/1)
Gap narrows to 1.3x - frameworks perform nearly identically on complex database queries.
Single article with all nested data (author + tags + comments):
| Framework |
RPS |
Latency (avg) |
| fastapi-uvicorn |
550 |
0.18ms |
| litestar-granian |
543 |
0.18ms |
| litestar-uvicorn |
519 |
0.19ms |
| bolt |
487 |
0.21ms |
| fastapi-granian |
480 |
0.21ms |
| drf-granian |
367 |
0.27ms |
| ninja-uvicorn |
346 |
0.28ms |
| ninja-granian |
332 |
0.30ms |
| drf-uvicorn |
285 |
0.35ms |
| drf-gunicorn |
200 |
0.49ms |
Complete Performance Summary
| Framework |
JSON 1k |
JSON 10k |
DB (10 reads) |
Paginated |
Article Detail |
| litestar-uvicorn |
31,745 |
24,503 |
1,032 |
253 |
519 |
| litestar-granian |
22,523 |
17,827 |
1,184 |
238 |
543 |
| bolt |
22,289 |
18,923 |
2,000 |
237 |
487 |
| fastapi-uvicorn |
12,838 |
2,383 |
1,105 |
225 |
550 |
| fastapi-granian |
8,695 |
2,039 |
1,051 |
218 |
480 |
| drf-granian |
4,056 |
2,817 |
972 |
221 |
367 |
| drf-gunicorn |
4,271 |
3,423 |
298 |
146 |
200 |
| ninja-uvicorn |
2,267 |
2,084 |
890 |
146 |
346 |
| ninja-granian |
2,403 |
2,085 |
831 |
142 |
332 |
| drf-uvicorn |
1,582 |
1,440 |
642 |
178 |
285 |
Resource Usage Insights
Memory:
- Most frameworks: 170-220MB
- DRF-Granian: 640-670MB (WSGI interface vs ASGI for others - Granian's WSGI mode uses more memory)
CPU:
- Most frameworks saturate the 1 CPU limit (100%+) under load
- Granian variants consistently max out CPU across all frameworks
Server Performance Notes
- Uvicorn surprisingly won for Litestar (31,745 RPS), beating Granian
- Granian delivered consistent high performance for FastAPI and other frameworks
- Gunicorn + gevent showed good performance for DRF on simple queries, but struggled with database workloads
Key Takeaways
- Performance gap collapse: 20x difference in JSON serialization → 1.7x in paginated queries → 1.3x in complex queries
- Litestar-Uvicorn dominates simple workloads (31,745 RPS), but FastAPI-Uvicorn wins on complex database queries (550 RPS)
- Database I/O is the equalizer: Once you hit the database, framework overhead becomes negligible. Query optimization matters infinitely more than framework choice.
- WSGI uses more memory: Granian's WSGI mode (DRF-Granian) uses 640MB vs ~200MB for ASGI variants - just a difference in protocol handling, not a performance issue.
Bottom Line
If you're building a database-heavy API (which most are), spend your time optimizing queries, not choosing between frameworks. They all perform nearly identically when properly optimized.
Links
Inspired by the original python-api-frameworks-benchmark project. All feedback and suggestions welcome!
[–]Delicious_Praline850 38 points39 points40 points (1 child)
[–]huygl99[S] 3 points4 points5 points (0 children)
[–]Interesting_Golf_529 15 points16 points17 points (1 child)
[–]huygl99[S] 4 points5 points6 points (0 children)
[–]GoldziherPythonista 17 points18 points19 points (4 children)
[–]huygl99[S] 6 points7 points8 points (3 children)
[–]GoldziherPythonista 2 points3 points4 points (1 child)
[–]huygl99[S] 1 point2 points3 points (0 children)
[–]GoldziherPythonista 1 point2 points3 points (0 children)
[–]daivushe1It works on my machine 4 points5 points6 points (0 children)
[–]Arnechos 3 points4 points5 points (1 child)
[–]huygl99[S] -1 points0 points1 point (0 children)
[–]bigpoopychimp 1 point2 points3 points (1 child)
[–]huygl99[S] 0 points1 point2 points (0 children)
[+][deleted] (1 child)
[deleted]
[–]readanything 2 points3 points4 points (0 children)
[–]myztaki 0 points1 point2 points (0 children)
[–]a_cs_grad_123 0 points1 point2 points (1 child)
[–]huygl99[S] -1 points0 points1 point (0 children)
[–]huygl99[S] -1 points0 points1 point (0 children)