ASGI vs WSGI: Web Server Interfaces
Understand the infrastructure that powers Python web applications - from traditional synchronous WSGI to modern asynchronous ASGI.
Why Understanding Server Interfaces Matters
In Lesson 2, you built your first API with FastAPI. But what's happening under the hood? How does your Python code actually communicate with web servers like Nginx or Apache? The answer lies in server interfaces: **WSGI** (Web Server Gateway Interface) and **ASGI** (Asynchronous Server Gateway Interface).
Understanding these interfaces helps you choose the right framework, server, and deployment strategy for your APIs. It explains why FastAPI can handle WebSockets while Flask cannot, and why some servers are faster than others.
Prerequisites
This lesson builds on Lesson 2 (Building Your First API with FastAPI). You should understand basic HTTP concepts and have created a simple API endpoint.
What is WSGI? (Traditional Synchronous Interface)
History and Purpose
WSGI (Web Server Gateway Interface) was defined in PEP 333 (2003) to standardize how Python web applications communicate with web servers. Before WSGI, each web framework had its own way of talking to servers, making deployment a nightmare.
The WSGI Architecture
WSGI acts as a contract between web servers and Python applications, allowing any WSGI-compliant server to run any WSGI-compliant application.
How WSGI Works
A WSGI application is simply a Python callable (function or class) that accepts two arguments and returns an iterable of bytes.
# Minimal WSGI application
def application(environ, start_response):
"""
environ: Dictionary containing HTTP request information
start_response: Callable to send HTTP response headers
"""
# Read request information
method = environ['REQUEST_METHOD']
path = environ['PATH_INFO']
# Prepare response
status = '200 OK'
headers = [('Content-Type', 'application/json')]
# Send headers
start_response(status, headers)
# Return response body as iterable of bytes
response_body = b'{"message": "Hello from WSGI!"}'
return [response_body]
# To run with a WSGI server:
# gunicorn myapp:applicationKey WSGI Concepts:
- environ: Dictionary with request data (headers, method, path, query strings)
- start_response: Function to set status code and headers
- Return value: Iterable of byte strings (response body)
- Synchronous: Each request blocks until completion
Popular WSGI Frameworks
Flask
Micro-framework for simple APIs
from flask import Flask app = Flask(__name__)
Django
Full-featured web framework (also supports ASGI since 3.0+)
# settings.py WSGI_APPLICATION = 'mysite.wsgi'
Pyramid
Flexible, modular framework
from pyramid.config import Configurator
WSGI Limitations
Why WSGI Falls Short for Modern Apps
- Synchronous Only: Each request blocks a worker thread/process
- No WebSocket Support: Can't maintain persistent connections
- No HTTP/2 Multiplexing: Limited to traditional request/response
- Poor Long-Polling Support: Ties up workers for long-running requests
- No async/await: Cannot use modern Python async features
- Scalability Issues: One worker per concurrent request = high memory usage
# WSGI blocks on I/O operations
import time
def wsgi_app(environ, start_response):
# This blocks the entire worker!
time.sleep(5) # Simulating database query
start_response('200 OK', [('Content-Type', 'text/plain')])
return [b'Done!']
# Problem: While sleeping, this worker can't handle other requests
# Solution: Need more workers = more memory = doesn't scale wellWhat is ASGI? (Modern Async Interface)
Why ASGI Was Created
ASGI (Asynchronous Server Gateway Interface) was created to overcome WSGI's limitations. Introduced by Django Channels team in 2016 and formalized in modern Python (3.5+), ASGI brings async/await support to web applications.
What ASGI Enables
- WebSockets: Bi-directional, persistent connections for real-time apps
- HTTP/2: Multiplexing and header compression (native support in servers like Hypercorn)
- Long-polling: Efficient handling of long-running requests
- Async/await: Non-blocking I/O with Python's async features
- Better Scalability: Handle thousands of concurrent connections
- Background Tasks: Lifespan events for startup/shutdown tasks
How ASGI Works
An ASGI application is an async callable that accepts three parameters: scope, receive, and send.
# Minimal ASGI application
async def application(scope, receive, send):
"""
scope: Dictionary with connection information (type, method, path, headers)
receive: Async callable to receive messages from client
send: Async callable to send messages to client
"""
if scope['type'] == 'http':
# Handle HTTP request
await send({
'type': 'http.response.start',
'status': 200,
'headers': [
[b'content-type', b'application/json'],
],
})
await send({
'type': 'http.response.body',
'body': b'{"message": "Hello from ASGI!"}',
})
elif scope['type'] == 'websocket':
# Handle WebSocket connection
await send({'type': 'websocket.accept'})
message = await receive()
await send({
'type': 'websocket.send',
'text': 'Echo: ' + message['text']
})
# To run with an ASGI server:
# uvicorn myapp:applicationKey ASGI Concepts:
- scope: Connection metadata (HTTP, WebSocket, or lifespan)
- receive: Async function to get messages from client
- send: Async function to send messages to client
- Asynchronous: Uses async/await for non-blocking I/O
- Protocol agnostic: Supports HTTP, WebSocket, HTTP/2, etc.
Popular ASGI Frameworks
FastAPI
Modern, high-performance API framework
from fastapi import FastAPI app = FastAPI()
Starlette
Lightweight ASGI framework
from starlette.applications import Starlette app = Starlette()
Channels
Django with async support
# settings.py ASGI_APPLICATION = 'myproject.asgi.application'
Lifespan Events
ASGI introduces lifespan events for application startup and shutdown - perfect for database connections, cache initialization, and cleanup.
# FastAPI lifespan events
from fastapi import FastAPI
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: runs once when application starts
print("🚀 Connecting to database...")
db = await connect_to_database()
yield # Application runs here
# Shutdown: runs once when application stops
print("🛑 Closing database connection...")
await db.close()
app = FastAPI(lifespan=lifespan)
@app.get("/")
async def read_root():
return {"message": "API is running"}WSGI vs ASGI: Side-by-Side Comparison
| Feature | WSGI | ASGI |
|---|---|---|
| Introduced | 2003 (PEP 333) | 2016 (Django Channels) |
| Execution Model | Synchronous (blocking) | Asynchronous (non-blocking) |
| Concurrency | One request per worker | Thousands of connections per worker |
| WebSocket Support | ❌ Not supported | ✅ Full support |
| HTTP/2 | ⚠️ Via reverse proxy only | ✅ Native + reverse proxy |
| Long-Polling | ❌ Blocks workers | ✅ Efficient |
| Async/Await | ❌ Not supported | ✅ Native support |
| Popular Servers | Gunicorn, uWSGI, mod_wsgi | Uvicorn, Hypercorn, Daphne |
| Popular Frameworks | Flask, Django (traditional), Pyramid | FastAPI, Starlette, Django 3.0+, Channels |
| Memory Usage | Higher (more workers needed) | Lower (single worker handles more) |
| Learning Curve | Simpler (traditional Python) | Steeper (async/await concepts) |
| Best For | Traditional web apps, simple APIs | Real-time apps, high-concurrency APIs |
Performance Implications
WSGI Performance
# CPU-bound task (e.g., heavy computation) # WSGI: Good ✅ # Uses multiple workers/processes Workers: 4 Requests/sec: ~1000 Memory: ~500MB
Best for: CPU-intensive tasks where each request does heavy computation
ASGI Performance
# I/O-bound task (e.g., database, APIs) # ASGI: Excellent ✅ # Async handles I/O efficiently Workers: 1 Requests/sec: ~5000 Memory: ~100MB
Best for: I/O-heavy tasks with many concurrent connections
Important: ASGI doesn't make CPU-bound code faster. For CPU-intensive work, use multiprocessing or Celery regardless of WSGI/ASGI choice.
When to Use Each
Choose WSGI When...
- Building a traditional web application (Django, Flask)
- You don't need WebSockets or long-polling
- Your team isn't familiar with async/await
- Using legacy dependencies that aren't async-compatible
- Simple CRUD APIs with low concurrency
- Mature ecosystem and extensive library support needed
Choose ASGI When...
- Building real-time features (chat, notifications, live updates)
- Need WebSocket support
- High concurrency with many I/O operations
- Modern API with async database drivers (asyncpg, motor)
- Starting a new project (future-proof)
- Want better performance with lower resource usage
Migration Path: Flask → FastAPI
If you're moving from WSGI to ASGI, here's how similar code compares:
Flask (WSGI)
from flask import Flask, jsonify
import requests
app = Flask(__name__)
@app.route("/users/<int:user_id>")
def get_user(user_id):
# Blocking I/O call
response = requests.get(
f"https://api.example.com/users/{user_id}"
)
user_data = response.json()
return jsonify(user_data)
@app.route("/health")
def health():
return jsonify({"status": "ok"})
if __name__ == "__main__":
app.run()FastAPI (ASGI)
from fastapi import FastAPI
import httpx
app = FastAPI()
@app.get("/users/{user_id}")
async def get_user(user_id: int):
# Non-blocking I/O call
async with httpx.AsyncClient() as client:
response = await client.get(
f"https://api.example.com/users/{user_id}"
)
user_data = response.json()
return user_data
@app.get("/health")
async def health():
return {"status": "ok"}
# Run with: uvicorn main:appMigration Tip: You can run FastAPI endpoints both sync and async. Start by converting I/O-heavy endpoints to async, leave CPU-bound ones as regular functions.
Practical Examples: Running Different Servers
WSGI Servers
# app.py - Flask application
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return {"message": "Hello from Flask"}
# Run with different WSGI servers:
# 1. Development server (built-in, not for production)
# python -m flask run
# 2. Gunicorn (production-ready)
# pip install gunicorn
# gunicorn -w 4 -b 0.0.0.0:8000 app:app
# -w 4: 4 worker processes
# -b: bind to host:port
# 3. uWSGI (alternative production server)
# pip install uwsgi
# uwsgi --http :8000 --wsgi-file app.py --callable app --master
# 4. With Nginx (reverse proxy)
# Nginx → Gunicorn → Flask appASGI Servers
# main.py - FastAPI application
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def hello():
return {"message": "Hello from FastAPI"}
# Run with different ASGI servers:
# 1. Uvicorn (most popular, recommended)
# pip install uvicorn
# uvicorn main:app --host 0.0.0.0 --port 8000
# With auto-reload for development:
# uvicorn main:app --reload
# 2. Uvicorn with multiple workers (production)
# uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
# 3. Hypercorn (alternative ASGI server)
# pip install hypercorn
# hypercorn main:app --bind 0.0.0.0:8000
# 4. Daphne (Django Channels server)
# pip install daphne
# daphne -b 0.0.0.0 -p 8000 main:app
# 5. With Nginx (reverse proxy)
# Nginx → Uvicorn → FastAPI appProduction Deployment Comparison
WSGI Production Stack
# Config: gunicorn.conf.py workers = 4 worker_class = 'sync' bind = '127.0.0.1:8000' timeout = 30
ASGI Production Stack
# Command: uvicorn main:app \\ --workers 4 \\ --host 127.0.0.1 \\ --port 8000
Key Takeaways
- WSGI (2003): Traditional synchronous interface for Python web apps
- ASGI (2016): Modern async interface enabling WebSockets, HTTP/2, and high concurrency
- FastAPI uses ASGI - that's why it can handle async/await and WebSockets
- Flask uses WSGI - simpler but can't do real-time features natively
- WSGI servers: Gunicorn, uWSGI (for Flask, Django)
- ASGI servers: Uvicorn, Hypercorn (for FastAPI, Starlette)
- Choose ASGI for: Real-time apps, high concurrency, modern APIs
- Choose WSGI for: Traditional web apps, simpler deployment, team familiarity
- Both can coexist: Use the right tool for each service in microservices architecture
What's Next?
Now that you understand the infrastructure layer, you're ready to dive deeper into:
- Request/Response Design: How to structure API requests and responses effectively
- WebSockets (Lesson 10): You'll understand why ASGI is required for real-time features
- Performance Optimization: Leverage async patterns for high-throughput APIs
- Deployment: Configure production servers (Nginx + Uvicorn/Gunicorn) properly