- Author: Amin Boulouma, Software Engineer
- Github source code: https://github.com/aminblm/ai_systems_design_from_scratch
- Engineering Blog: https://aminblm.github.io/ai_systems_design_from_scratch/blog/
ASGI Servers: Beyond WSGI
For decades, Python web development was dominated by WSGI (Web Server Gateway Interface). However, WSGI was designed for synchronous, request-response cycles. It cannot handle long-lived connections like WebSockets or HTTP/2 streams. When your application needs to hold thousands of concurrent connections—the classic setup for the “midnight deployment spike”—WSGI becomes a bottleneck.
ASGI (Asynchronous Server Gateway Interface) is the spiritual successor to WSGI. It provides an asynchronous interface that allows your Python application to handle millions of events without blocking.
The Theory: The ASGI Specification
The ASGI specification splits the communication into two parts:
- Connection Scope: Information about the connection (headers, path, metadata).
- Event Queue: A stream of messages sent between the server and the application (e.g.,
http.request,websocket.receive).
Glossary for Beginners
- ASGI: The bridge between a web server (like Uvicorn) and your Python code.
- WSGI: The “old” bridge, which only knows how to do one thing at a time.
- WebSockets: A way for the server to talk to the browser in real-time (like a phone call instead of sending letters).
- Concurrency: Doing many things at the same time, switching between them so fast it looks like they are happening at once.
Simple Implementation: A Minimal ASGI App
This is the simplest form of an ASGI application, functioning as an asynchronous callable.
async def app(scope, receive, send):
# scope: connection info
# receive: read from the client
# send: write to the client
assert scope['type'] == 'http'
await send({
'type': 'http.response.start',
'status': 200,
'headers': [[b'content-type', b'text/plain']],
})
await send({
'type': 'http.response.body',
'body': b'Hello, ASGI World!',
})
Complex Implementation: Production-Grade Middleware
In enterprise setups, you often need middleware to inject headers or handle authentication across the entire ASGI stack.
class AuthenticationMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
# Validate headers before passing to the inner app
headers = dict(scope.get('headers', []))
if b'authorization' not in headers:
await send({'type': 'http.response.start', 'status': 401})
await send({'type': 'http.response.body', 'body': b'Unauthorized'})
return
await self.app(scope, receive, send)
Quick Reference: ASGI vs. WSGI
| Feature | WSGI | ASGI |
|---|---|---|
| Concurrency | Synchronous (Thread-per-request) | Asynchronous (Event loop) |
| Support | HTTP/1.1 | HTTP/1.1, HTTP/2, WebSockets |
| Throughput | Capped by thread count | High (Handles thousands of connections) |
| Complexity | Low | Medium/High |
Why We Choose ASGI over WSGI
We choose ASGI because it offers Native Concurrency. In a microservices architecture, our services often need to push notifications to clients or maintain long-lived streams to other services. ASGI handles these states natively. WSGI requires workarounds (like long-polling or external message queues) that add significant latency to our deployment architecture.
Developer Checklist
- Are you using an ASGI-compliant server (e.g., Uvicorn, Daphne, or Hypercorn)?
- Is your application code fully asynchronous (no
time.sleepor blocking I/O)? - Have you configured your reverse proxy (Nginx) for WebSocket support?
- Does your application gracefully handle disconnects (websocket close events)?
Takeaways
- Asynchronicity: The web is now async; stop using blocking gateway interfaces.
- Protocol Flexibility: ASGI allows you to build WebSocket-based real-time systems easily.
- Performance: High-concurrency performance is an architectural requirement, not an optimization.