Введение

WSGI и ASGI — то, на чем стоит весь современный веб на Python. Это стандарты, которые описывают интерфейс между веб-сервером и приложением. Благодаря им сервер и фреймворк не образуют жесткую пару: любой WSGI-сервер запускает любое WSGI-приложение, любой ASGI-сервер любое ASGI-приложение. Uvicorn не знает ничего о FastAPI, FastAPI не знает ничего о Uvicorn, они знают только о том, что передать на вход и что ожидать на выходе.

В этой статье мы разберем:

  • зачем в Python вообще понадобились такие стандарты и как появился WSGI

  • почему WSGI уперся в потолок с приходом WebSocket и асинхронности

  • как устроен ASGI изнутри: общий интерфейс scope, receive, send и три субспецификации (HTTP, WebSocket, Lifespan)

  • напишем минимальные ASGI-приложения и запустим их на Uvicorn

В конце заглянем в исходники Starlette и увидим тот же интерфейс в промышленном коде.

Зоопарк фреймворков и серверов, или Как в Python появился стандарт WSGI

В начале 2000-х Python уже был популярным языком, а веб-разработка на нем живой и разнообразной. Существовали Zope, Quixote, Webware, Twisted Web и другие фреймворки. Казалось бы, богатый выбор — это круто. Но на практике это создавало серьезную проблему, которую авторы PEP 333 (2003 год) сформулировали так:

Широкое разнообразие вариантов может стать проблемой для новых пользователей Python, поскольку, как правило, выбор веб-фреймворка ограничивает выбор доступных веб-серверов, и наоборот.

Фреймворк и сервер были жестко связаны. Хочешь запустить Zope? Используй конкретный сервер. Хочешь перейти на другой сервер? Переписывай интеграцию. Каждая новая пара "фреймворк + сервер" требовала отдельной инженерной работы.

Авторы PEP посмотрели на Java и увидели там решение: Servlet API. Любой Java-фреймворк работает на любом Java-сервере, потому что между ними есть стандартный контракт (тот самый Servlet API). Python нуждался в том же.

В 2003 году Филлип Эби (Phillip J. Eby) предложил Web Server Gateway Interface, сокращенно WSGI ("виски"). Ключевой принцип был сформулирован так:

Простота реализации как на стороне сервера, так и на стороне фреймворка является абсолютно критичной для полезности интерфейса WSGI, и поэтому является главным критерием при любых проектных решениях.

Простота тут не случайное слово. Весь контракт WSGI, который должны соблюдать программа-сервер и программа-приложение, сводится к 2 требованиям.

От фреймворка (приложения) требуется предоставить вызываемый объект (функцию, класс или инстанс с методом __call__), который принимает ровно 2 аргумента:

  • первый аргумент environ обычный словарь с данными запроса: метод, путь, заголовки, тело. Его формат унаследован от CGI: серверы уже умели формировать такие переменные, фреймворки — их читать. Переиспользовали готовое.

  • второй аргумент start_response тоже вызываемый объект, который фреймворк обязан вызвать до того, как вернет тело ответа. В него передаются статус и заголовки. После этого приложение возвращает итерируемый объект с телом ответа.

def application(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return [b'Hello, World!']

От сервера требуется зеркальное: сформировать словарь environ из входящего HTTP-запроса, передать его в приложение вместе с объектомstart_response, получить от приложения итерируемый объект и отдать байты клиенту.

Если фреймворк соблюдает эти требования, он WSGI-совместим. Если сервер соблюдает эти требования, он тоже WSGI-совместим. Как говорил Аркадий Райкин: "Я тебя уважаю, ты меня уважаешь. Мы с тобой уважаемые люди". И тогда любая пара "сервер + фреймворк" работает без какого-либо адаптера.

Вот иллюстрация из самого PEP: CGI-шлюз, который запускает любое WSGI-приложение:

def run_with_cgi(application):
    environ = {k: unicode_to_wsgi(v) for k, v in os.environ.items()}
    environ['wsgi.input']   = sys.stdin.buffer
    environ['wsgi.errors']  = sys.stderr
    environ['wsgi.version'] = (1, 0)
    # ...

    result = application(environ, start_response)
    for data in result:
        if data:
            write(data)

Обратите внимание: сервер не знает ничего о фреймворке. Фреймворк не знает ничего о сервере. Они знают только об этом контракте.

Отдельно стоит упомянуть middleware ("промежуточные слои"). PEP 333 ввел этот термин и дал ему такое определение:

Помимо "чистых" серверов/шлюзов и приложений/фреймворков, возможно создание компонентов "middleware", реализующих обе стороны этой спецификации. Такие компоненты ведут себя как приложение по отношению к содержащему их серверу и как сервер по отношению к содержащемуся в них приложению.

Именно это делает middleware элегантным: каждый такой компонент ничего не знает о соседях, знает только о WSGI-контракте. Это позволяет выстраивать цепочки произвольной длины: аутентификация - логирование - роутинг - само приложение. В PEP эта конструкция названа middleware stack ("стек промежуточных слоев"), и ее присутствие, как отмечается в стандарте, "прозрачно для обеих сторон интерфейса и не требует какой-либо специальной поддержки".

Результат оказался именно таким, каким задумывался: любой WSGI-сервер (Gunicorn, uWSGI, mod_wsgi, встроенный сервер Django) запускает любое WSGI-приложение (Django, Flask, Pyramid, Falcon, Bottle) без каких-либо адаптеров. Выбор сервера и выбор фреймворка стали независимыми решениями.

WSGI был принят в 2003 году как PEP 333, а в 2010-м обновлён до PEP 3333 — преимущественно для корректной работы с Python 3 и уточнения работы со строками и байтами.

Почему стало не хватать WSGI

WSGI прекрасно справлялся со своей задачей почти 10 лет. Но у него есть фундаментальное ограничение, заложенное в самой его природе: он построен вокруг модели "один запрос - один ответ".

Посмотрим на сигнатуру еще раз:

def application(environ, start_response):
    ...
    return [b'тело ответа']

Сервер вызвал приложение, приложение вернуло ответ, история закончена. Соединение закрыто, поток свободен. Для обычных HTTP-запросов это работает отлично.

Но веб к тому времени уже шагнул дальше:

  • появились веб-сокеты (WebSocket) — протокол, при котором соединение между клиентом и сервером остается открытым. Данные идут в обе стороны в любой момент: сервер может отправить сообщение клиенту без какого-либо запроса с его стороны. Чаты, онлайн-игры, совместное редактирование документов — все это веб-сокеты. В модель "вызвал - вернул - забыл" этот протокол не вписывается никак;

  • HTTP/2 добавил мультиплексирование: одно TCP-соединение может нести несколько запросов одновременно. Модель WSGI об этом ничего не знает, ведь она работает с одним запросом за раз;

  • лонг-поллинг и SSE (Server-Sent Events) — техники, при которых сервер намеренно держит соединение открытым, постепенно отправляя данные. В синхронной модели WSGI это означает занятый поток на все время соединения.

Авторы ASGI-спецификации сформулировали проблему дизайна WSGI так:

Дизайн WSGI неразрывно связан с циклом HTTP-запрос/ответ, а все больше протоколов, не следующих этой модели, становятся стандартной частью веб-программирования, в первую очередь WebSocket.

WSGI отлично решает ту задачу, для которой был создан. Но веб вырос и от Python потреб��вались новые решения. Этим решением стал ASGI.

На сцену выходит ASGI

ASGI ("асги") расшифровывается как Asynchronous Server Gateway Interface. Это не замена WSGI, а его эволюция. В спецификации отмечено:

ASGI пытается сохранить простой интерфейс приложений, обеспечивая при этом абстракцию, которая позволяет отправлять и получать данные в любой момент и из разных потоков или процессов приложения.

ASGI не имеет такого официального статуса как WSGI (не существует PEP на ASGI). Спецификацию разработал Эндрю Годвин (Andrew Godwin), автор Django Channels который решал конкретную задачу: научить Django работать с веб-сокетами. Том Кристи позже создал Uvicorn и Starlette, доказав что ASGI жизнеспособен за пределами Django. Несмотря на неофициальный статус, ASGI стал де-факто стандартом: Uvicorn, Starlette, FastAPI, Daphne, Hypercorn, Django — все реализуют ASGI-спецификацию.

WSGI-приложение говорит с сервером один раз: получило запрос, вернуло ответ. ASGI-приложение разговаривает с сервером на протяжении всего времени жизни соединения, обмениваясь сообщениями-событиями туда и обратно.

Контракт при этом остался таким же лаконичным, просто не 2 аргумента, а 3:

async def application(scope, receive, send):
    ...

Заметили async в сигнатуре? Это принципиальное отличие от WSGI. ASGI-приложение — корутина, работающая в событийном цикле asyncio. Сервер не блокируется в ожидании ответа, а продолжает обрабатывать другие соединения, пока приложение делает свою работу.

Три вышеуказанных аргументв составляют три понятия, вокруг которых построена вся спецификация ASGI:

  • scope — словарь с информацией о текущем соединении. Всегда содержит ключ type, по которому приложение понимает, с каким протоколом работает: HTTP, WebSocket или что-то еще. Это аналог environ из WSGI, но с важным отличием: для веб-сокетов scope живет все время существования соединения, а не только один запрос.

  • receive — асинхронный вызываемый объект, через который приложение получает входящие события от сервера. Вызвал await receive() — получил словарь с очередным событием: пришли данные от клиента, соединение закрылось и так далее.

  • send — асинхронный вызываемый объект для отправки событий обратно серверу. Вызвали await send({...}), сервер передал данные клиенту.

Спецификация поясняет:

Приложения вызываются со словарем, содержащим информацию о соединении (аргумент scope) и двумя awaitable-объектами для получения сообщений о событии (аргумент receive) и отправки таких сообщений в ответ (аргумент send). Все это происходит в асинхронном цикле событий.

Если WSGI — это телефонный звонок ("позвонил, ответил, положил трубку"), то ASGI — это мессенджер: соединение открыто, сообщения летят в обе стороны, и каждая сторона реагирует по мере поступления событий.

ASGI не монолитная спецификация, а набор из нескольких подспецификаций. Базовый документ фиксирует общий интерфейс: 3 аргумента, словари событий, асинхронный контракт. А конкретные протоколы описаны отдельно.

Сейчас их три: спецификация HTTP — классические запрос-ответ, включая HTTP/2; спецификация WebSocket — двустороннее соединение, которое живёт столько, сколько нужно; Lifespan — события жизненного цикла приложения, запуск и остановка. Приложение узнает с каким протоколом работает по ключу scope["type"], это всегда первое, что оно должно проверять.

Минимальное ASGI-приложение для HTTP

Проиллюстрируем ASGI-спецификацию со стороны приложения. Представим голую async-функцию (она и будет нашим приложением), которую можно будет поставить в пару с любым ASGI-сервером.

# myapp.py

async def application(scope, receive, send):
    # ждём события от сервера (тело запроса)
    await receive()

    # отправляем статус и заголовки
    await send({
        "type": "http.response.start",
        "status": 200,
        "headers": [
            [b"content-type", b"text/plain"],
        ],
    })

    # отправляем тело ответа
    await send({
        "type": "http.response.body",
        "body": b"Hello, ASGI!",
    })

Важный момент: receive и send — это не функции, которые пишет разработчик приложения. Это вызываемые объекты, которые сервер (например, Uvicorn) передает в наше приложение при каждом новом соединении. Сервер реализует их сам, за ними стоит вся низкоуровневая работа с сокетами, буферами и событийным циклом. Наша задача как разработчиков приложения только вызывать их в нужный момент.

await receive() — ждем от сервера событие типа http.request. В нем тело запроса. В нашем микроприложении тело запроса не нужно. Мы вызываем receive(), чтобы показать как приложение получает событие. Технически для простого ответа этот вызов необязателен.

Дальше 2 вызова await send(). Не один вызов, как в WSGI, а два последовательных события:

  • сначала http.response.start, отправка статуса и заголовков

  • потом http.response.body, отправка тела.

Это позволяет серверу отправить заголовки клиенту немедленно, не дожидаясь, пока приложение сформирует тело целиком.

Каждое событие — это просто словарь с обязательным ключом type. Спецификация строго определяет, какие ключи ожидаются в каждом типе события. Заголовки передаются как список пар байтовых строк.

Возьмем любой ASGI-сервер (например, Uvicorn) и запустим наше микроприложение:

uvicorn myapp:application

Uvicorn будет слушать порт, принимать TCP-соединения, формировать scope из входящих HTTP-запросов и вызывать нашу корутину.

Запустив с��рвер мы увидим, что наше микроприложение подхватилось, сервер не падает и по умолчанию слушает на 8000:

$ uvicorn myapp:application
INFO:     Started server process [4760]
INFO:     Waiting for application startup.
INFO:     ASGI 'lifespan' protocol appears unsupported.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

Сделаем запрос и посмотрим и заголовки, и тело ответа

$ curl -i http://127.0.0.1:8000
HTTP/1.1 200 OK
date: Tue, 24 Feb 2026 17:34:39 GMT
server: uvicorn
content-type: text/plain
Transfer-Encoding: chunked

Hello, ASGI!

Наше приложение штатно отработало.

Один нюанс: "INFO: ASGI 'lifespan' protocol appears unsupported.". Uvicorn попробовал отправить нашему приложению событие запуска (lifespan), а мы его не обработали. Lifespan тоже часть ASGI-спецификации, о ней поговорим чуть позже.

Минимальное ASGI-приложение для WebSocket

Вот где ASGI раскрывается по-настоящему! Посмотрим на то же самое приложение, но для веб-сокетов и сразу станет видно, насколько элегантно спецификация обобщает разные протоколы.

# myapp_ws.py

async def application(scope, receive, send):
    # принимаем входящее соединение
    # событие websocket.connect
    await receive()

    await send({
        "type": "websocket.accept",
    })

    # ждем сообщение от клиента
    # событие websocket.receive
    event = await receive()

    # делаем эхо
    await send({
        "type": "websocket.send",
        "text": event["text"],
    })

Интерфейс тот же самый: приложение получает от сервера объекты scope, receive, send. Но теперь события другие.

Сначала await receive() возвращает событие websocket.connect. Клиент постучался и ждет рукопожатия. В ответ мы отправляем websocket.accept, соединение установлено.

Потом снова await receive(), но теперь это уже websocket.receive, входящее сообщение от клиента. Читаем текст из event["text"] и отправляем его обратно через websocket.send. Классическое эхо.

Обратите внимание: никакой магии, только словари с ключом type. Спецификация определяет набор событий для каждого протокола, приложение просто реагирует на них в нужном порядке.

Запускаем приложение

$ uvicorn myapp_ws:application
INFO:     Started server process [14532]
INFO:     Waiting for application startup.
INFO:     ASGI 'lifespan' protocol appears unsupported.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

Проверим работу приложения, используя Python-пакет websockets (pip install websockets).

Запускаем

python -m websockets ws://127.0.0.1:8000

Видим

Connected to ws://127.0.0.1:8000.
>

Клиент готов передавать сообщения. Поскольку наше серверное приложение настроено на эхо, его и ожидаем увидеть

Connected to ws://127.0.0.1:8000.
> привет!
< привет!
Connection closed: 1006 (abnormal closure [internal]).

Сервер принял соединение, получил сообщение и вернул его обратно.

Тем временем в консоли сервера

$ uvicorn myapp_ws:application
...
INFO:     127.0.0.1:52690 - "WebSocket /" [accepted]
INFO:     connection open
INFO:     connection closed

С учетом учебного характера примера все отработало штатно. Наше приложение получило одно сообщение, ответило и завершилось. Сервер закрыл соединение со своей стороны, клиент это зафиксировал. Код 1006 означает, что соединение завершилось без корректного завершающего рукопожатия, предусмотренного протоколом веб-сокетов.

В реальном приложении, чате или игре, соединение должно жить дольше. Для этого достаточно обернуть обработку событий в цикл и слушать websocket.disconnect:

async def application(scope, receive, send):
    await receive()  # websocket.connect
    await send({"type": "websocket.accept"})

    while True:
        event = await receive()

        if event["type"] == "websocket.disconnect":
            break

        await send({
            "type": "websocket.send",
            "text": event["text"],
        })
$ python -m websockets ws://127.0.0.1:8000
Connected to ws://127.0.0.1:8000.
> привет!
< привет!
> и еще разок!
< и еще разок!
> и еще!
< и еще!

Lifespan — жизненный цикл приложения

Вернемся к предупреждению, которое мы видели при запуске Uvicorn:

INFO: ASGI 'lifespan' protocol appears unsupported.

Lifespan третья подспецификация в ASGI наряду с HTTP и WebSocket. Она отвечает за события жизненного цикла приложения: запуск и остановку.

Спецификация объясняет зачем это нужно:

Сообщения жизненного цикла позволяют приложению инициализироваться и завершать работу в контексте работающего цикла событий. Примером может быть создание пула соединений и последующее его закрытие для освобождения соединений.

Типичный сценарий: при старте приложения нужно открыть пул соединений с базой данных, при остановке аккуратно закрыть. Lifespan обеспечивает правильный момент.

Механика та же: события через receive и send, только scope["type"] теперь равен "lifespan":

async def application(scope, receive, send):
    if scope["type"] == "lifespan":
        while True:
            message = await receive()

            if message["type"] == "lifespan.startup":
                # инициализация: подключаемся к БД, открываем кэш...
                await send({"type": "lifespan.startup.complete"})

            elif message["type"] == "lifespan.shutdown":
                # очистка: закрываем соединения...
                await send({"type": "lifespan.shutdown.complete"})
                return

    elif scope["type"] == "http":
        ...  # обычная обработка запросов

Приложение получает lifespan.startup, выполняет инициализацию и отвечает lifespan.startup.complete. Сервер ждет этого подтверждения перед тем, как начать принимать запросы. Та же история при остановке, сервер не завершается, пока не получит lifespan.shutdown.complete.

Именно поэтому Uvicorn предупреждал нас в прошлых примерах: он попробовал отправить lifespan.startup, наше приложение его проигнорировало, сервер понял что lifespan не поддерживается и продолжил работу без него.

Как ASGI выглядит в реальном коде: заглядываем в Starlette

Мы написали минимальные ASGI-приложения руками. Теперь посмотрим как тот же контракт реализован в промышленном коде.

Starlette — это ASGI-фреймворк, который берет на себя рутину: роутинг, обработку запросов, middleware. FastAPI построен поверх Starlette, добавляя валидацию через Pydantic и автогенерацию OpenAPI, но сам ASGI-слой не трогает. Поэтому если хочется понять как ASGI реализован в FastAPI, смотреть нужно в Starlette.

Заглянем в репозиторийй Starlette.

Первое, что бросается в глаза, знакомая сигнатура. Класс Router реализует метод app, и он выглядит так:

async def app(self, scope: Scope, receive: Receive, send: Send) -> None:

Три аргумента scope, receive, send. Это и есть следование ASGI-спецификации: любой ASGI-совместимый фреймворк обязан принимать именно их. Starlette не исключение.

Дальше внутри метода уже знакомая логика:

assert scope["type"] in ("http", "websocket", "lifespan")

if scope["type"] == "lifespan":
    await self.lifespan(scope, receive, send)
    return

for route in self.routes:
    match, child_scope = route.matches(scope)
    if match == Match.FULL:
        await route.handle(scope, receive, send)
        return

Тот самый scope["type"], который мы проверяли вручную. Starlette делает ровно то же самое, просто оборачивает это в класс с роутингом.

Роутер нашел подходящий маршрут и передал управление обработчику. Но как обычная функция вида def handler(request) превращается в ASGI-приложение? Вот тут:

def request_response(func):
    async def app(scope: Scope, receive: Receive, send: Send) -> None:
        request = Request(scope, receive, send)
        response = await f(request)
        await response(scope, receive, send)
    return app

Starlette берет обычную функцию def handler(request) и оборачивает ее в полноценное ASGI-приложение с scope/receive/send.

Здесь проявляется ключевая идея ASGI: любой объект с сигнатурой async def(scope, receive, send) является ASGI-приложением. Это значит, что их можно вкладывать друг в друга как матрешку:

Uvicorn (серве��)
    └── Router (ASGI-приложение)
            ├── handler_1 (ASGI-приложение)
            ├── handler_2 (ASGI-приложение)
            ├── handler_3 (ASGI-приложение)
            └── ...

Uvicorn вызывает Router как ASGI-приложение. Router находит нужный маршрут и вызывает обработчик тоже как ASGI-приложение. Каждый уровень получает те же три аргумента и передает их дальше.

Именно на этом принципе строится middleware: это ASGI-приложение, которое оборачивает другое ASGI-приложение. Оно перехватывает вызов, делает свое дело (логирует, проверяет авторизацию, меняет заголовки) и передаёт управление следующему уровню. Цепочка может быть сколь угодно длинной, и каждое звено ничего не знает о соседях, только о контракте scope/receive/send.

Заключение

WSGI и ASGI являются архитектурными решениями, которые определили весь облик современного Python-веба.

WSGI в 2003 году решил важную задачу: разорвал жесткую связь между сервером и фреймворком. Один стандартный контракт: два аргумента, один вызываемый объект. Теперь любая пара "сервер + фреймворк" могла работать без адаптеров, если соблюдала соглашение.

ASGI почти 15 лет спустя сделал следующий шаг: сохранил ту же идею, но расширил ее до асинхронного мира. Три аргумента вместо двух, корутина вместо обычной функции, поток событий вместо одного запроса-ответа. И тот же принцип: любой объект, соблюдающий контракт scope/receive/send, является ASGI-приложением — будь то роутер, обработчик маршрута или middleware.

Именно поэтому Uvicorn запускает FastAPI, Starlette, и любое другое async-приложение без каких-либо дополнительных настроек. Они все говорят на одном языке.

Что еще почитать (из того, что встретил на Хабре)

Обзор WSGI, ASGI и RSGI: лидеры среди веб-серверов в 2025 году — если интересно сравнить конкретные серверы по производительности и узнать про экспериментальный интерфейс RSGI.

Введение в ASGI: становление асинхронной веб-экосистемы Python — переводная статья 2019 года, хорошее введение в экосистему ASGI.