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

ClientBrowser / Mobile AppWeb ServerNginx / Apache (Reverse Proxy)WSGI ServerGunicorn / uWSGI / mod_wsgiWSGI AppFlask / Django (Your Code)HTTP RequestWSGI ProtocolPython Call

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:application
Key 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 well

What 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:application
Key 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

FeatureWSGIASGI
Introduced2003 (PEP 333)2016 (Django Channels)
Execution ModelSynchronous (blocking)Asynchronous (non-blocking)
ConcurrencyOne request per workerThousands 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 ServersGunicorn, uWSGI, mod_wsgiUvicorn, Hypercorn, Daphne
Popular FrameworksFlask, Django (traditional), PyramidFastAPI, Starlette, Django 3.0+, Channels
Memory UsageHigher (more workers needed)Lower (single worker handles more)
Learning CurveSimpler (traditional Python)Steeper (async/await concepts)
Best ForTraditional web apps, simple APIsReal-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:app

Migration 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 app

ASGI 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 app

Production Deployment Comparison

WSGI Production Stack
NginxReverse Proxy, SSLGunicornWSGI Server, 4 workersFlask AppYour Code
# Config: gunicorn.conf.py
workers = 4
worker_class = 'sync'
bind = '127.0.0.1:8000'
timeout = 30
ASGI Production Stack
NginxReverse Proxy, SSLUvicornASGI Server, 4 workersFastAPI AppYour Code
# 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