WSGI и ASGI: sync vs async серверы

Когда пишешь веб-приложение на Python, между твоим кодом и HTTP-клиентом стоит веб-сервер. Чтобы они могли общаться, нужен стандартный интерфейс. WSGI (Web Server Gateway Interface) - синхронный стандарт с 2003 года. ASGI (Async Server Gateway Interface) - современный async-стандарт. В этом уроке - что они делают, чем отличаются и почему для нового кода обычно выбирают ASGI.

Зачем нужен интерфейс

Веб-сервер (gunicorn, uvicorn) принимает HTTP-запросы, парсит их и должен передать приложению. Приложение обрабатывает и возвращает response. Без стандарта каждое приложение нужно было бы вручную адаптировать под каждый сервер.

WSGI/ASGI - это контракт: какие функции вызывать, что передавать, что получать. Приложение реализует контракт, сервер вызывает по нему.

WSGI - синхронный стандарт

Минимальное WSGI-приложение:

def app(environ, start_response):
    start_response("200 OK", [("Content-Type", "text/plain")])
    return [b"Hello, World!"]

environ - dict с информацией о запросе (URL, headers, method). start_response - функция для статуса и headers. Возврат - iterable байтов.

Запускается через WSGI-сервер:

pip install gunicorn
gunicorn myapp:app

gunicorn принимает HTTP-запросы, конвертирует в WSGI вызовы, передаёт твоему приложению. На production - стандарт для синхронных Python-приложений (Django, Flask по умолчанию).

Frameworks на WSGI

# Flask - WSGI framework
from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello"

# Django тоже WSGI (хотя поддерживает ASGI с 3.0)

WSGI-frameworks: Flask, Django (legacy mode), Pyramid, Bottle.

Проблема WSGI - синхронность

WSGI спроектирован под синхронную модель: один worker = один запрос за раз. Чтобы обработать 1000 одновременных connections, нужно 1000 workers - это много памяти.

def app(environ, start_response):
    # Если внутри блокирующая операция (БД-запрос, HTTP-вызов),
    # этот worker stuck до её завершения
    data = slow_db_query()    # блокирует
    return [data.encode()]

Для I/O-heavy приложений (API gateway, веб-скрейпер, real-time chat) синхронная модель неэффективна.

ASGI - асинхронный стандарт

ASGI разработан в 2017 году для async Python. Поддерживает HTTP, WebSocket, lifespan events.

async def app(scope, receive, send):
    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, World!",
    })
  • scope - информация о connection (как WSGI environ)
  • receive - awaitable для чтения данных (тела запроса, WebSocket-сообщений)
  • send - awaitable для отправки

Запускается через ASGI-сервер:

pip install uvicorn
uvicorn myapp:app

uvicorn - быстрый ASGI-сервер. Альтернативы: hypercorn, daphne. Именно uvicorn мы кладём в контейнер в уроке про Docker и CI.

Frameworks на ASGI

# FastAPI - ASGI framework
from fastapi import FastAPI
app = FastAPI()

@app.get("/")
async def hello():
    return {"message": "Hello"}

# Starlette - lightweight ASGI
# Django 3.0+ поддерживает оба

ASGI-frameworks: FastAPI, Starlette, Quart, Sanic, Django 3.0+ (опционально).

Главное преимущество ASGI

ASGI обрабатывает много параллельных connections одним worker'ом через event loop. Для I/O-bound нагрузки 1 ASGI worker может заменить 100 WSGI workers.

@app.get("/data")
async def get_data():
    # Все три запроса параллельно - не блокируют worker
    users, orders, stats = await asyncio.gather(
        fetch_users(),
        fetch_orders(),
        fetch_stats(),
    )
    return {"users": users, "orders": orders, "stats": stats}

Это особенно важно для microservices - сервис делает много вызовов к другим сервисам, и ASGI справляется без огромного количества workers.

Сравнение производительности

СценарийWSGI (gunicorn)ASGI (uvicorn)
CPU-bound, простойЛучшеСравнимо
I/O-bound, много APIХуже (много workers)Лучше (один worker)
WebSocketНе поддерживаетНативно
Простота кодаПривычнееasync/await требует понимания
Эко-системаЗрелая (Django, Flask)Растёт быстро (FastAPI)

Для нового backend в 2026 году обычно выбирают ASGI (FastAPI). Для существующих Django-проектов часто остаются на WSGI.

ASGI поддерживает WebSocket

async def app(scope, receive, send):
    if scope["type"] == "websocket":
        await send({"type": "websocket.accept"})
        while True:
            event = await receive()
            if event["type"] == "websocket.receive":
                text = event.get("text", "")
                await send({"type": "websocket.send", "text": f"Echo: {text}"})
            elif event["type"] == "websocket.disconnect":
                break

WSGI вообще не поддерживает WebSocket - нужны хаки или отдельный сервер. ASGI обрабатывает в одном протоколе.

Развёртывание - workers и async

WSGI: запускаешь N процессов (workers), каждый обрабатывает один запрос за раз:

gunicorn myapp:app --workers 4

ASGI: один процесс может обрабатывать тысячи concurrent connections. Для CPU-параллельности всё равно нужны несколько workers:

uvicorn myapp:app --workers 4

Но каждый worker - это event loop, не один-запрос-за-раз.

Mixed - WSGI приложение в ASGI

Можно запустить WSGI-приложение через ASGI-сервер через WsgiToAsgi adapter:

from asgiref.wsgi import WsgiToAsgi
from flask import Flask

flask_app = Flask(__name__)
asgi_app = WsgiToAsgi(flask_app)

# Теперь можно запускать через uvicorn:
# uvicorn myapp:asgi_app

Это compat-mode - не даёт async-преимуществ внутри Flask-handler'ов, но позволяет, например, использовать ASGI-middleware.

Stack изнутри FastAPI

HTTP request
  ↓
uvicorn (ASGI server) - парсит HTTP, создаёт scope/receive/send
  ↓
Starlette (ASGI framework) - routing, request/response
  ↓
FastAPI (Starlette + Pydantic) - validation, OpenAPI docs
  ↓
Твой код (async def handler)

Слои чётко разделены. Можно теоретически использовать каждый слой отдельно, но обычно работаешь только с FastAPI.

Middleware

В ASGI middleware - функция-обёртка вокруг app:

class LoggingMiddleware:
    def __init__(self, app):
        self.app = app

    async def __call__(self, scope, receive, send):
        if scope["type"] == "http":
            print(f"Request: {scope['method']} {scope['path']}")
        await self.app(scope, receive, send)

# Применение
app = FastAPI()
app.add_middleware(LoggingMiddleware)

WSGI middleware похожий, но синхронный.

Встроенные middleware в FastAPI: CORS, GZip, Trusted Host, Session, etc.

Lifespan events

ASGI поддерживает startup/shutdown события:

from fastapi import FastAPI
from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup
    print("Starting up")
    app.state.db = await connect_db()
    yield
    # Shutdown
    print("Shutting down")
    await app.state.db.close()

app = FastAPI(lifespan=lifespan)

Удобно для setup ресурсов (DB pools, кеши) и cleanup. В WSGI такого стандарта нет - всё через init модуля.

Когда WSGI vs ASGI

Используй WSGI:

  • Существующий Django проект без async needs
  • Простой синхронный код без heavy I/O
  • Команда не знакома с async

Используй ASGI:

  • Новый проект backend в 2026
  • Много API-вызовов (microservices)
  • WebSocket, Server-Sent Events
  • Real-time приложения
  • Хочется FastAPI с Pydantic

Распространённые ошибки

1. Blocking sync в ASGI handler

@app.get("/data")
async def bad():
    time.sleep(5)              # БЛОКИРУЕТ event loop
    requests.get("...")        # БЛОКИРУЕТ (синхронный requests)

Внутри async-handler'а нельзя вызывать blocking sync. Используй await asyncio.sleep, aiohttp вместо requests. Или вынеси в executor:

result = await asyncio.to_thread(blocking_function, arg)

2. Mixing WSGI и ASGI в одном проекте без понимания

Можно через адаптеры, но overhead и сложность. Обычно проще выбрать один стек.

3. Не использовать lifespan для cleanup

@app.get("/")
def handler():
    db = Database()           # создаётся на каждый запрос - очень медленно
    return db.query(...)

Используй lifespan для общих ресурсов (DB pool создаётся раз).

Сравнение с Go

В Go стандартная библиотека сразу async через горутины:

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello"))
})
http.ListenAndServe(":8080", nil)

Каждый запрос обрабатывается в отдельной горутине - параллельно из коробки. Нет WSGI/ASGI - Go's net/http это и сервер, и интерфейс.

В Python WSGI/ASGI нужны потому что Python был не designed для concurrency и история тащит синхронный код. Go это уровень выше для веба, Python требует выбора стека.

Мини-задание

  1. Минимальное WSGI-приложение:
# wsgi_app.py
def app(environ, start_response):
    method = environ["REQUEST_METHOD"]
    path = environ["PATH_INFO"]
    start_response("200 OK", [("Content-Type", "text/plain")])
    return [f"{method} {path}".encode()]

# Запуск
# pip install gunicorn
# gunicorn wsgi_app:app
  1. Минимальное ASGI-приложение:
# asgi_app.py
async def app(scope, receive, send):
    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 from ASGI",
    })

# Запуск
# pip install uvicorn
# uvicorn asgi_app:app
  1. Адаптер WSGI → ASGI:
from asgiref.wsgi import WsgiToAsgi

def wsgi_app(environ, start_response):
    start_response("200 OK", [("Content-Type", "text/plain")])
    return [b"Hello"]

app = WsgiToAsgi(wsgi_app)
# Теперь можно: uvicorn module:app

Что дальше

Поняли что WSGI/ASGI это интерфейсы между сервером и приложением. В следующем уроке - FastAPI: современный ASGI-фреймворк, который мы будем использовать в этом и последующих уроках.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.