Skip to main content

Web Frameworks: WSGI and ASGI Explained

Web frameworks are Python libraries that abstract away low-level HTTP details, letting you focus on application logic. They bridge the gap between a web server (Nginx, Apache) and your Python code using standardized interfaces. WSGI (Web Server Gateway Interface) handles synchronous applications and remains the standard for Flask and Django. ASGI (Asynchronous Server Gateway Interface) is the modern successor, enabling async/await patterns and WebSocket support in frameworks like FastAPI. Understanding these specifications helps you choose the right framework and deploy correctly.

Key Takeaways​

  • Web frameworks handle HTTP parsing, routing, and request/response formatting automatically
  • WSGI is the synchronous standard: one request per worker, blocking I/O (Flask, Django)
  • ASGI is the asynchronous standard: concurrent requests, async/await, WebSockets (FastAPI, Starlette)
  • Application servers (Gunicorn for WSGI, Uvicorn for ASGI) run between web servers and Python code
  • Production stack: Client <-> Web Server <-> Application Server <-> Python App
  • WSGI good for traditional request-response; ASGI good for high-concurrency I/O-bound tasks

What Is a Web Framework and Why Do You Need One?​

A web framework is a Python library that provides a structured way to build web applications by handling the low-level details of HTTP communication. Without a framework, you'd manually parse raw HTTP requests, route URLs to functions, manage headers, and format responses—tedious and error-prone. A framework handles all this infrastructure so you write clean, business-logic-focused Python code.

What frameworks do for you:

  1. Parse HTTP requests — extract path, headers, query strings, form data, JSON bodies
  2. Route URLs to functions — map /users/123 to a specific Python handler
  3. Provide request context — easy access to headers, cookies, POST data inside your function
  4. Format HTTP responses — convert your function's return value into proper HTTP with headers and status codes
  5. Handle common tasks — templating, sessions, authentication, database ORM, static file serving

Without a framework, you'd write low-level socket code:

import socket

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('localhost', 8000))
server_socket.listen()

while True:
client_socket, addr = server_socket.accept()
request_data = client_socket.recv(1024).decode('utf-8')

# Manually parse request: extract path, method, headers
lines = request_data.split('\r\n')
method, path, protocol = lines[0].split()

# Route and handle request manually
if path == '/hello':
response = b'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nHello, World!'
else:
response = b'HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\n\r\nNot Found'

client_socket.sendall(response)
client_socket.close()

With a framework (Flask), the same is trivial:

from flask import Flask

app = Flask(__name__)

@app.route('/hello')
def hello():
return 'Hello, World!'

if __name__ == '__main__':
app.run()

What Is WSGI and When Should You Use It?​

WSGI (Web Server Gateway Interface) is the long-standing standard for synchronous Python web applications. Defined by PEP 3333, WSGI specifies how a web server communicates with a Python application through a callable (function or object). WSGI servers like Gunicorn manage multiple worker processes, each handling one request at a time. Flask and Django are the most popular WSGI frameworks.

WSGI characteristics:

  • Synchronous: Handles one request per worker process; all operations are blocking
  • Standard: Well-established, battle-tested, widely deployed in production
  • Per-request workers: A worker accepts a request, processes it fully, returns a response, then accepts the next request
  • Simple model: Easy to understand; plain function-based routing

How WSGI works:

# A WSGI application is simply a callable that takes (environ, start_response)
def simple_wsgi_app(environ, start_response):
"""
A minimal WSGI application.

Args:
environ: Dict with request details (REQUEST_METHOD, PATH_INFO, etc.)
start_response: Callable to send HTTP status and headers
"""
# Extract request info from environ
method = environ['REQUEST_METHOD']
path = environ['PATH_INFO']

# Simple routing
if path == '/hello':
status = '200 OK'
response_body = b'Hello, World!'
else:
status = '404 Not Found'
response_body = b'Not Found'

# Call start_response to send status and headers
response_headers = [('Content-Type', 'text/plain')]
start_response(status, response_headers)

# Return response body as iterable
return [response_body]

Flask as a WSGI framework:

Flask wraps this low-level interface in a user-friendly API. When you run flask run or gunicorn app:app, Flask provides the WSGI callable, and the server handles worker management.

from flask import Flask, request

app = Flask(__name__)

@app.route('/greet', methods=['GET'])
def greet():
name = request.args.get('name', 'World')
return f'Hello, {name}!'

@app.route('/data', methods=['POST'])
def receive_data():
data = request.get_json()
return {'received': data}, 200

What Is ASGI and Why Is It Modern?​

ASGI (Asynchronous Server Gateway Interface) is the modern successor to WSGI, designed for asynchronous Python applications using asyncio. ASGI enables true concurrency without blocking, making it ideal for I/O-bound tasks, WebSockets, and high-traffic scenarios. FastAPI, Starlette, and Quart are popular ASGI frameworks.

ASGI characteristics:

  • Asynchronous: Uses async/await; thousands of concurrent connections per process
  • Event-driven: Handles multiple requests and long-lived connections concurrently
  • WebSocket support: Enables real-time bidirectional communication
  • Modern: Designed for contemporary web patterns (streaming, long-polling, real-time)

How ASGI works:

# An ASGI application is an async callable that takes (scope, receive, send)
async def simple_asgi_app(scope, receive, send):
"""
A minimal ASGI application.

Args:
scope: Dict with connection metadata (type, path, method, etc.)
receive: Async callable to receive messages from client
send: Async callable to send messages to client
"""
assert scope['type'] == 'http'

# Simple routing
path = scope['path']
if path == '/hello':
await send({
'type': 'http.response.start',
'status': 200,
'headers': [[b'content-type', b'text/plain']],
})
await send({
'type': 'http.response.body',
'body': b'Hello, World!',
})
else:
await send({
'type': 'http.response.start',
'status': 404,
'headers': [[b'content-type', b'text/plain']],
})
await send({
'type': 'http.response.body',
'body': b'Not Found',
})

FastAPI as an ASGI framework:

FastAPI abstracts the low-level interface into a Pythonic API with automatic API documentation.

from fastapi import FastAPI

app = FastAPI()

@app.get('/greet')
async def greet(name: str = 'World'):
"""Greet a user asynchronously."""
return {'message': f'Hello, {name}!'}

@app.websocket('/ws')
async def websocket_endpoint(websocket):
"""WebSocket endpoint for real-time communication."""
await websocket.accept()
while True:
data = await websocket.receive_text()
await websocket.send_text(f'Echo: {data}')

How Do Application Servers Connect Your Code to the Web Server?​

Application servers (Gunicorn, Uvicorn) are separate processes that run your Python application and speak the WSGI/ASGI protocol. They sit between the web server (Nginx, Apache) and your framework, managing worker processes, load balancing, and graceful restarts. This separation of concerns keeps the web server focused on static files and SSL, while the application server runs your code.

Production architecture:

Client Request
↓
Web Server (Nginx/Apache)
↓
Application Server (Gunicorn/Uvicorn)
├─ Worker Process 1 (runs Flask/FastAPI)
├─ Worker Process 2 (runs Flask/FastAPI)
├─ Worker Process 3 (runs Flask/FastAPI)
└─ Worker Process 4 (runs Flask/FastAPI)
↓
Your Python Application
↓
Database / Cache / External APIs

Gunicorn (WSGI):

Gunicorn spawns multiple worker processes, each running a synchronous WSGI application. It distributes incoming requests round-robin across workers. Each worker handles one request completely before accepting the next.

# Launch Flask with Gunicorn (4 workers)
gunicorn -w 4 -b 0.0.0.0:8000 app:app

# app:app means module 'app.py', variable 'app' (the Flask instance)

Uvicorn (ASGI):

Uvicorn is a fast ASGI server optimized for async frameworks. It can handle thousands of concurrent connections in a single process because it doesn't block on I/O.

# Launch FastAPI with Uvicorn
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4

# For async workloads, fewer workers needed than WSGI

WSGI vs. ASGI: When to Choose Each​

AspectWSGIASGI
Concurrency ModelSynchronous, blockingAsynchronous, non-blocking
Worker ProcessesMultiple workers (1 request each)Fewer workers, handles many requests per process
Best ForTraditional request-response APIsHigh-concurrency I/O, WebSockets, real-time
Popular FrameworksFlask, DjangoFastAPI, Starlette, Quart
ScalingAdd worker processesAdd processes or machines; scales better per process
Learning CurveEasy, straightforwardSteeper; requires async/await knowledge
Production MaturityBattle-tested, very stableGrowing adoption, production-ready

Choose WSGI if:

  • Building a traditional REST API or web application
  • Team is unfamiliar with async/await
  • Deployment is simpler with existing WSGI infrastructure
  • You prefer Flask's lightweight, familiar design

Choose ASGI if:

  • Need WebSocket support or real-time features
  • High concurrency (thousands of simultaneous connections)
  • Building async/streaming applications
  • Performance is critical and async is advantageous
  • Using modern frameworks like FastAPI

Frequently Asked Questions​

Can I use async/await with a WSGI framework?​

Django 3.1+ has ASGI support, allowing async views alongside synchronous ones. Flask doesn't natively support async, but you can use libraries like Quart (async reimplementation) or async wrappers. For true async, use ASGI frameworks like FastAPI.

Do I need to run a separate web server in front of my application server?​

It's recommended but not always required. In development, frameworks run their built-in servers. In production, use Nginx/Apache in front for SSL termination, static file serving, load balancing, and security. This separation allows you to restart the application server without downtime.

What happens if my WSGI worker gets stuck on a slow request?​

The worker is blocked and can't handle other requests until it finishes. With multiple workers, other clients still get served. This is why ASGI is better for slow or long-lived connections—an async app doesn't block the entire process.

Can I use Gunicorn with FastAPI?​

FastAPI is ASGI-only. Gunicorn is a WSGI server, so it won't work directly. Use Uvicorn instead, or use gunicorn -k uvicorn.workers.UvicornWorker to run Uvicorn workers under Gunicorn's process management.

How many workers should I run?​

For WSGI (Gunicorn): typically 2 × CPU_count + 1. For ASGI (Uvicorn): typically 1-2 workers per CPU, since each handles more concurrency. Monitor and adjust based on load testing.

Further Reading​