Logo
Overview

Stateless и Stateful API: полный разбор с примерами и таблицей сравнения

May 30, 2026
8 min read

Stateless и Stateful — это не про «сервер всё забывает» и «сервер всё помнит». Это про то, кто отвечает за хранение контекста между запросами и какую архитектурную цену вы платите за это решение. На практике выбор между этими двумя моделями определяет, сможете ли вы масштабироваться горизонтально или упрётесь в потолок при росте нагрузки.

Если вы проектируете REST API — центральные принципы и конвенции разобраны в базовых принципах REST API. А здесь — объяснение stateless/stateful на конкретных примерах кода с пояснениями построчно. Текущий пост — более глубокий разбор: архитектурные последствия, масштабирование и осознанный выбор модели.

Что такое состояние в контексте API

Состояние — это контекст, который связывает разрозненные HTTP-запросы в единый пользовательский сценарий. Без состояния каждый запрос — изолированное событие, как отдельная страница в блокноте. С состоянием — это главы одной книги, где каждая следующая страница продолжает предыдущую.

Что может быть состоянием:

  • Сессия пользователя — факт аутентификации, время входа, роль, права доступа
  • Промежуточные данные — содержимое корзины до оформления заказа, незавершённая форма, черновик документа
  • Рабочий контекст — на каком шаге многошагового процесса находится клиент (визард, онбординг, оформление кредита)
  • Пользовательские настройки — выбранный язык, тема интерфейса, активные фильтры в каталоге

Главный вопрос: где это состояние хранить — на клиенте или на сервере? Ответ на него разделяет stateless и stateful архитектуры. И цена ошибки здесь — переписывание половины бэкенда.

Stateless: каждый запрос сам за себя

В stateless-модели сервер не хранит абсолютно никакой информации о клиенте между запросами. Каждый запрос обязан содержать все данные, необходимые для его обработки.

100%
graph TD
  A["Клиент 1"] -->|"GET /orders (JWT-токен)"| LB["Балансировщик нагрузки"]
  LB -->|"Запрос 1"| S1["Сервер API №1"]
  LB -->|"Запрос 2"| S2["Сервер API №2"]
  S1 -->|"SELECT * FROM orders"| DB["База данных
(общее состояние)"]
  S2 -->|"SELECT * FROM orders"| DB
  DB -->|"Данные заказов"| S1
  DB -->|"Данные заказов"| S2
  S1 -->|"200 OK + JSON"| A
  S2 -->|"200 OK + JSON"| A
  style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
  style LB fill:#f0a500,stroke:#c88400,color:#fff
  style S1 fill:#50c878,stroke:#3a9a5c,color:#fff
  style S2 fill:#50c878,stroke:#3a9a5c,color:#fff
  style DB fill:#7b68ee,stroke:#5a4db2,color:#fff

На диаграмме: два запроса от одного клиента приземляются на разные серверы — балансировщику без разницы, куда отправлять. Оба сервера успешно обрабатывают запросы. Секрет в том, что состояние лежит в базе данных — едином источнике правды, доступ к которому есть у каждого сервера. JWT-токен в заголовке подтверждает личность клиента, но сами серверы про этого клиента «забывают» сразу после ответа.

В базовых принципах REST API statelessness — один из ключевых архитектурных ограничений. REST без него формально перестаёт быть REST.

Как это выглядит в коде

// Stateless endpoint
app.get('/orders', async (req, res) => {
const token = req.headers.authorization;
const userId = verifyJWT(token);
const orders = await db.query(
'SELECT * FROM orders WHERE user_id = ?', [userId]
);
res.json(orders);
});

Каждая строчка делает ровно то, что написано: извлекли токен из заголовка, проверили, сходили в базу, вернули результат. Никакой памяти о предыдущем запросе. Никаких скрытых зависимостей между вызовами.

Сервер можно перезагрузить в любой момент — следующий запрос приедет с токеном, и всё продолжит работать как ни в чём не бывало.

Плюсы stateless

Горизонтальное масштабирование становится тривиальным. Добавили ещё 10 серверов за балансировщиком — и они сразу готовы к работе. Не нужно синхронизировать сессии между экземплярами, настраивать sticky sessions, думать о репликации состояния. Это основная причина, почему облачные платформы (AWS, GCP, Azure) спроектированы под stateless-сервисы. Kubernetes поднимает и убивает поды — и ничего не ломается.

Отказоустойчивость. Упал сервер API №1? Балансировщик перестал слать на него трафик. Клиент даже не заметил — его запрос ушёл на сервер №2. В stateful-мире клиент потерял бы сессию и увидел ошибку.

Простота тестирования. Каждый запрос изолирован, можно писать модульные тесты без поднятия Redis для сессий и сложного окружения. Меньше моков — быстрее тесты — выше покрытие.

Кэширование на стороне сервера. Поскольку ответ зависит только от переданных данных, результаты GET-запросов можно агрессивно кэшировать. CDN, nginx, Redis — все работают «из коробки».

Минусы

Плата за простоту — объём передаваемых данных. Токен аутентификации, параметры пагинации, идентификаторы ресурсов — всё это кочует из запроса в запрос. В высоконагруженных системах оверхед заметен. Плюс каждый запрос потенциально требует повторной загрузки данных из базы, которые в stateful-модели могли бы лежать в оперативной памяти.

Примечательно, что для большинства бизнес-приложений это приемлемая цена. Условный интернет-магазин на 10 000 заказов в день не почувствует разницы.

Stateful: сервер, который помнит

Stateful-модель предполагает, что сервер хранит состояние клиента между запросами в своей оперативной памяти или внешнем быстром хранилище (Redis, Memcached). Клиент передаёт только идентификатор сессии, а сервер восстанавливает контекст по этому идентификатору.

100%
sequenceDiagram
  participant C as Клиент
  participant S as Сервер
  participant M as Хранилище сессий
  participant D as База данных

  C->>S: POST /login (логин, пароль)
  S->>D: проверить пользователя
  D-->>S: user_id = 42
  S->>M: сохранить сессию sess_abc
  M-->>S: OK
  S-->>C: 200 OK + sess_abc

  C->>S: GET /cart + sess_abc
  S->>M: загрузить sess_abc
  M-->>S: user_id=42
  S->>D: SELECT cart WHERE user_id=42
  D-->>S: товары X, Y, Z
  S-->>C: 200 OK (корзина)

  C->>S: POST /checkout + sess_abc
  S->>M: загрузить sess_abc
  M-->>S: user_id=42
  S->>D: INSERT INTO orders
  D-->>S: order_id=789
  S-->>C: 200 OK (заказ создан)

Диаграмма показывает три запроса от клиента. Первый — логин, сервер создаёт сессию и отдаёт идентификатор sess_abc. Второй и третий запросы передают этот идентификатор, и сервер восстанавливает контекст из хранилища сессий. Обратите внимание: повторная аутентификация не нужна — идентификатор сессии её заменяет. В этом ключевое удобство stateful для клиента.

Когда stateful — зло (и когда нет)

В контексте REST API stateful считается антипаттерном. Но выкидывать stateful на свалку истории рано. Есть сценарии, где он оправдан на 100%:

WebSocket-соединения. Чат, онлайн-игра, биржевой терминал — здесь состояние соединения принципиально. Клиент и сервер держат открытый канал, и контекст (комната чата, игровая сессия, подписки на котировки) живёт на стороне сервера. Иначе сделать невозможно.

FTP, SSH и прочие протоколы с длинными сессиями. FTP открывает управляющее соединение, авторизуется один раз и дальше гоняет файлы. Stateful по стандарту — и никто не жалуется.

Банковские и платёжные шлюзы с двухфазными транзакциями. Иногда нужно зафиксировать промежуточное состояние (платёж инициирован, ожидается подтверждение от эквайера), и терять его между запросами нельзя. Здесь stateful — не баг, а требование безопасности.

Легаси-монолиты, которые никто не перепишет. «Работает — не трогай». Десятки тысяч строк кода с серверными сессиями, которые никто не даст рефакторить. Иногда дешевле купить ещё пару серверов, чем переписывать.

Подводные камни масштабирования stateful

Главная боль: если клиент «привязан» к конкретному экземпляру сервера (sticky session), то при падении этого экземпляра клиент теряет сессию. Приходится городить: репликацию сессий между экземплярами, внешние Redis-хранилища, сложные схемы фейловера.

При росте нагрузки нельзя просто взять и добавить серверов — нужно параллельно реплицировать сессионное состояние. Каждый новый экземпляр либо копирует память с соседей, либо ходит в общее Redis-хранилище. И то и другое добавляет сетевую задержку. В stateless-мире этой проблемы просто не существует.

Таблица сравнения

КритерийStatelessStateful
Хранение состоянияНа клиенте (токен, query-параметры)На сервере (память, Redis, БД)
МасштабированиеГоризонтальное, линейноеОграничено sticky sessions или стоимостью репликации
ОтказоустойчивостьВысокая — любой сервер обработает запросЗависит от механизма восстановления сессий
Простота тестированияИзолированные юнит-тестыТребуется контекст сессии
Объём данных в запросеВыше (токен, параметры, контекст)Ниже (только session ID)
Задержка (latency)Каждый запрос ходит в БДЧасть данных может быть в памяти
Рестарт сервераБез последствийПотеря сессий без персистентного хранилища
Типичные протоколыREST, GraphQLWebSocket, FTP, SSH, gRPC-стримы
ИдемпотентностьЛегко обеспечитьСложнее из-за зависимостей между запросами

Таблица не означает, что stateless всегда лучше. Stateful проигрывает по масштабированию — но выигрывает по latency для повторяющихся операций. Вопрос в том, что для вас критичнее.

Как сделать stateful более живучим

Если совсем без состояния нельзя (например, WebSocket-чат), есть несколько стратегий выживания:

  1. Вынос сессионного состояния в Redis/Memcached. Сервер всё ещё stateful, но сессии не теряются при рестарте и доступны с разных экземпляров. Минус — дополнительный сетевой вызов на каждый запрос, но для большинства приложений это доли миллисекунды.

  2. Stateless-ядро + stateful-надстройка. Основная бизнес-логика stateless. Состояние требуется только для отдельных сценариев (websocket-уведомления, реалтайм-дашборды). Тогда 95% запросов масштабируются горизонтально, а оставшиеся 5% живут на выделенных серверах с WebSocket.

  3. Распределённые сессии с аффинностью «best effort». Балансировщик старается отправлять клиента на тот же сервер (session affinity), но при падении сервера сессия восстанавливается из Redis. Компромисс между производительностью и надёжностью — и довольно популярный на практике.

Когда stateful — осознанный выбор

Несмотря на доминирование stateless в современной веб-разработке, есть моменты, когда stateful даёт ощутимый выигрыш:

  • Игровые серверы. В реальном времени состояние игры меняется десятки раз в секунду. Гонять его в БД на каждом тике — самоубийство. Игровой сервер держит состояние мира в оперативной памяти и синхронизирует с клиентами через UDP/WebSocket.

  • Потоковая обработка данных. Apache Flink, Kafka Streams — stateful по природе. Они держат окна агрегации, водяные знаки и чекпоинты в состоянии, и это фича, а не баг. Без состояния здесь просто невозможно посчитать скользящее среднее или детектировать аномалии в потоке.

  • AI-диалоговые системы и ассистенты. История диалога, извлечённые сущности, контекст текущей задачи — это состояние, которое живёт в рамках сессии общения с LLM. Хранение всего этого на клиенте неэффективно: в одном диалоге могут быть десятки сообщений с эмбеддингами и суммаризациями.

Это не серебряная пуля — каждый из этих кейсов требует отдельного инженерного решения для отказоустойчивости. Но осознанный stateful выигрывает у слепого stateless, когда домен требует состояния.

Заключение

Stateless и stateful — не враги и не конкуренты. Это инструменты с разной ценой владения. В мире REST API stateless — золотой стандарт: он упрощает масштабирование, тестирование и отказоустойчивость до уровня «просто добавь серверов». Но как только вы выходите за рамки «запрос-ответ» — WebSocket, стриминг, игры, диалоговые системы — stateful становится не проклятием, а осознанной необходимостью.

Главное — не вешать ярлык «плохо» на stateful. Спрашивайте: «Какую проблему мы решаем и какую цену платим за хранение состояния здесь, а не на клиенте?» Честный ответ на этот вопрос экономит месяцы переписывания архитектуры потом.

PS. Если после прочтения вы побежали переписывать stateful-сервис на stateless — перечитайте раздел про легаси. Возможно, оно того не стоит.