La diferencia entre WSGI y ASGI no es solo una cuestión de “síncrono vs asíncrono”: es el contrato que tu aplicación firma con el servidor, y ese contrato determina qué tipos de conexiones puedes manejar, cómo escala bajo carga y qué tecnologías de tiempo real están disponibles o simplemente prohibidas.
WSGI (definido en PEP 3333) es un callable Python que recibe environ y start_response, procesa la request de forma síncrona, y devuelve un iterable con el body. Un worker WSGI maneja exactamente una request a la vez; cuando esa llamada bloquea esperando la base de datos, el worker está muerto para todo lo demás. Gunicorn compensa esto lanzando múltiples procesos worker (tipicamente 2 * CPUs + 1), así que el paralelismo es real pero tiene un coste: cada worker es un proceso Python completo con su propio heap y su propio GIL.
ASGI (PEP no oficial, especificación en el repositorio de asgiref) introduce un contrato diferente: la aplicación es un callable async que recibe scope, receive y send. El scope describe la conexión (HTTP, WebSocket, lifespan); receive y send son coroutines que permiten al servidor y a la aplicación intercambiar mensajes de forma incremental. Un worker ASGI con asyncio puede tener miles de conexiones en vuelo simultáneamente porque mientras espera I/O en una, el event loop avanza con otras.
Esto habilita cosas que WSGI hace imposibles por diseño: WebSockets necesitan una conexión persistente bidireccional; Server-Sent Events requieren que la respuesta quede abierta indefinidamente; HTTP/2 push necesita multiplexar streams sobre una sola conexión TCP. WSGI no tiene modelo mental para ninguno de esos casos porque su interfaz asume que la respuesta termina y el worker queda libre.
La pregunta relevante no es “¿cuál es mejor?” sino “¿cuándo el overhead de ASGI no se justifica?”. Si tu aplicación es mayoritariamente request/response sin I/O concurrente, Flask con Gunicorn es más predecible, tiene mejor ecosistema de middleware maduro y el modelo de procesos aislados te da fault isolation gratis. ASGI brilla cuando el I/O concurrente es real: muchas conexiones simultáneas a APIs externas, WebSockets, o cuando ya estás usando asyncio en toda la stack.
# wsgi_vs_asgi_demo.py
# Requiere: pip install flask fastapi uvicorn gunicorn httpx anyio
import asyncio
import time
import httpx
from flask import Flask
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
# ── WSGI: Flask ──────────────────────────────────────────────────────────────
wsgi_app = Flask(__name__)
@wsgi_app.route("/wsgi/sequential")
def wsgi_sequential():
"""
Cada llamada a time.sleep bloquea el worker completo.
Con gunicorn -w 4, solo 4 requests pueden ejecutarse en paralelo.
"""
time.sleep(0.1) # simula I/O bloqueante (base de datos, API externa)
time.sleep(0.1)
return {"total_sleep": "200ms", "interface": "WSGI"}
# ── ASGI: FastAPI ────────────────────────────────────────────────────────────
asgi_app = FastAPI()
@asgi_app.get("/asgi/concurrent")
async def asgi_concurrent():
"""
asyncio.sleep cede el control al event loop; el worker puede procesar
otras requests mientras espera. Un solo proceso maneja miles de estas.
"""
await asyncio.sleep(0.1)
await asyncio.sleep(0.1)
return {"total_sleep": "200ms", "interface": "ASGI"}
@asgi_app.get("/asgi/fan-out")
async def asgi_fan_out():
"""
Fan-out a tres APIs externas en paralelo: imposible sin async en WSGI
sin lanzar threads explícitamente.
"""
async with httpx.AsyncClient() as client:
results = await asyncio.gather(
client.get("https://httpbin.org/delay/1"),
client.get("https://httpbin.org/delay/1"),
client.get("https://httpbin.org/delay/1"),
)
# Los tres requests salen al mismo tiempo; el tiempo total es ~1s, no ~3s.
return {"statuses": [r.status_code for r in results]}
async def event_stream():
"""
Server-Sent Events: la conexión permanece abierta y el servidor empuja
mensajes. WSGI no puede modelar esto; ASGI lo hace de forma natural
porque send/receive son streams, no un único par request/response.
"""
for i in range(5):
await asyncio.sleep(1)
yield f"data: tick {i}\n\n" # formato SSE estándar
@asgi_app.get("/asgi/sse")
async def asgi_sse():
return StreamingResponse(event_stream(), media_type="text/event-stream")
# ── Inspección del contrato de cada interfaz ─────────────────────────────────
def inspect_wsgi_contract(app):
"""
Un servidor WSGI llama a la aplicación así de simple.
environ es un dict con los headers y metadatos HTTP.
start_response es un callable que la app invoca para fijar status y headers.
"""
responses = []
def start_response(status, headers):
responses.append({"status": status, "headers": headers})
environ = {
"REQUEST_METHOD": "GET",
"PATH_INFO": "/wsgi/sequential",
"SERVER_NAME": "localhost",
"SERVER_PORT": "8000",
"wsgi.input": __import__("io").BytesIO(b""),
"wsgi.errors": __import__("sys").stderr,
"wsgi.url_scheme": "http",
"wsgi.multithread": False,
"wsgi.multiprocess": True,
"wsgi.run_once": False,
}
body = b"".join(app(environ, start_response))
return responses[0]["status"], body
async def inspect_asgi_contract(app):
"""
Un servidor ASGI llama a la aplicación con scope/receive/send.
receive y send son coroutines: el protocolo es un diálogo, no un monólogo.
"""
scope = {
"type": "http",
"asgi": {"version": "3.0"},
"http_version": "1.1",
"method": "GET",
"path": "/asgi/concurrent",
"query_string": b"",
"headers": [],
}
messages = []
async def receive():
# El servidor entrega el body de la request cuando la app lo pide
return {"type": "http.request", "body": b"", "more_body": False}
async def send(message):
# La app empuja mensajes al servidor: primero headers, luego body
messages.append(message)
await app(scope, receive, send)
return messages
if __name__ == "__main__":
# Demostración del contrato WSGI
status, body = inspect_wsgi_contract(wsgi_app.wsgi_app)
print(f"WSGI status: {status}")
# Demostración del contrato ASGI
messages = asyncio.run(inspect_asgi_contract(asgi_app))
response_start = next(m for m in messages if m["type"] == "http.response.start")
print(f"ASGI status: {response_start['status']}")
Qué está pasando realmente aquí
El punto de entrada de inspect_wsgi_contract te muestra el contrato en su forma más desnuda: la aplicación es una función que el servidor llama una vez, de forma síncrona, y cuando retorna la respuesta está completa. No hay forma de que la app diga “espera, todavía tengo más datos”; la única salida es el iterable que devuelve.
inspect_asgi_contract revela por qué ASGI puede hacer más: la app recibe send como herramienta para empujar mensajes al servidor en cualquier momento. Fíjate en cómo event_stream usa yield en un generador async: cada yield llama a send internamente vía StreamingResponse, manteniendo la conexión abierta y enviando datos cuando están listos. Eso no es un truco de FastAPI, es lo que el protocolo ASGI hace posible por diseño.
El endpoint /asgi/fan-out ilustra el caso más común donde ASGI gana de forma objetiva. Con WSGI y Flask necesitarías concurrent.futures.ThreadPoolExecutor para hacer tres llamadas HTTP en paralelo, lo cual funciona pero añade complejidad y consume threads del OS. Con ASGI el fan-out es asyncio.gather: un solo thread del OS, sin context switches de kernel, con el mismo efecto.
La elección de servidor refleja directamente esta dicotomía. Gunicorn con --workers 4 arranca cuatro procesos Python; cada uno puede procesar una request bloqueante simultáneamente. Uvicorn con --workers 1 arranca un proceso con un event loop que maneja miles de conexiones concurrentes siempre que el I/O sea async. En producción se suele combinar Gunicorn como gestor de procesos con Uvicorn como worker class (gunicorn -k uvicorn.workers.UvicornWorker), obteniendo fault isolation de procesos más concurrencia dentro de cada uno.
El overhead de ASGI que mencioné antes es concreto: el event loop de asyncio tiene latencia de scheduling, el modelo mental de “todo debe ser async o bloqueas el loop” es una restricción real, y librerías síncronas como psycopg2 o el ORM clásico de Django bloquean el worker si las llamas sin asyncio.run_in_executor. Si tu app hace queries SQL, sirve HTML renderizado y no tiene WebSockets ni I/O masivamente paralelo, Flask + Gunicorn te evita esa clase de accidentes.
Errores que debes conocer
Error: llamar código síncrono bloqueante desde un handler async, matando la concurrencia que ASGI prometía.
# ❌ Wrong
import psycopg2
@asgi_app.get("/users")
async def get_users():
conn = psycopg2.connect("dbname=mydb") # bloquea el event loop entero
cur = conn.cursor()
cur.execute("SELECT * FROM users")
return cur.fetchall()
# ✅ Right
import asyncpg
@asgi_app.get("/users")
async def get_users():
conn = await asyncpg.connect("postgresql://localhost/mydb")
rows = await conn.fetch("SELECT * FROM users")
await conn.close()
return [dict(r) for r in rows]
Usar un driver async (asyncpg, aiosqlite, aiomysql) garantiza que la llamada cede al event loop mientras espera la base de datos.
Error: desplegar una app ASGI con un servidor WSGI puro (o viceversa), lo que produce errores crípticos en runtime en lugar de un fallo claro al arrancar.
# ❌ Wrong: gunicorn no entiende el contrato ASGI gunicorn myapp:asgi_app # ✅ Right: uvicorn para ASGI, o gunicorn con el worker class correcto uvicorn myapp:asgi_app --host 0.0.0.0 --port 8000 # o en producción multi-worker: gunicorn myapp:asgi_app -k uvicorn.workers.UvicornWorker -w 4
El worker class de Uvicorn dentro de Gunicorn es el puente: Gunicorn gestiona los procesos y las señales del OS, Uvicorn implementa el protocolo ASGI dentro de cada proceso.
N° 180