Request ID (идентификатор запроса)

18 октября 2026 · ~15 мин чтения

концепция observability отладка api трассировка

Request ID (идентификатор запроса)

Request ID (идентификатор запроса) — короткая уникальная строка, которая присваивается одному запросу в момент входа в систему и передаётся вместе с ним через все сервисы, очереди и фоновые задачи. По ней потом собирают из логов и ответов одну связную историю: кто отправил, кто принял, где сломалось.

История

Идея появилась не в вебе, а в интеграции корпоративных систем, когда выяснилось, что асинхронные сообщения нельзя сопоставить с ответами «на глаз».

Отдельно про сам заголовок X-Request-ID: это де-факто конвенция, а не стандарт. В IETF был черновик draft-ietf-httpapi-request-id, но до RFC он, насколько я знаю, не дошёл. Поэтому в разных компаниях живут X-Request-ID, X-Correlation-ID, Request-Id, X-Amzn-RequestId, cf-ray — и это одна из главных практических болей.

Что это такое

Request id — это не способ авторизации, не кэш-ключ и не защита от повторов. Это метка «это всё про один и тот же запрос», которую удобно положить в каждую строчку лога.

Разберём пары, которые чаще всего путают.

Request ID vs Trace ID. Request id — идентификатор запроса, то есть события «клиент пришёл». Trace id — идентификатор трассы, то есть всего распределённого сценария, который этот запрос породил. В простом случае это одно и то же значение, и так обычно и делают: берут request id и объявляют его trace id. Разница проявляется, когда один входящий запрос порождает несколько независимых ветвей (например, fan-out на десять сервисов): трасса одна, а внутри неё десятки span id — по одному на каждый шаг.

Request ID vs Idempotency Key. Idempotency key (ключ идемпотентности) — это идентификатор намерения: «списать деньги по этому заказу ровно один раз». Он живёт дольше запроса и переживает повторные отправки: если клиент повторил запрос с тем же ключом, сервер вернёт сохранённый результат, а не выполнит операцию второй раз. Request id, наоборот, уникален для каждой попытки: две попытки — два разных request id. Это разные поля, и подменять одно другим — классическая ошибка.

Request ID vs Session ID. Session id живёт часами и объединяет много запросов одного пользователя. Request id живёт секунды и объединяет много сервисов внутри одного запроса. Они ортогональны: полезно логировать оба.

Request ID vs бизнес-ключ. order_id, invoice_id, user_id — это доменные сущности, они существуют независимо от запроса. Request id нужен там, где бизнес-ключа ещё нет (запрос упал до создания заказа) или где бизнес-ключей несколько.

Главное свойство request id — он должен быть дешёвым и неинтересным. 16–32 символа, без смысла, без персональных данных. Если в идентификаторе зашит e-mail клиента или номер договора — это уже утечка, а не идентификатор.

Аналогии из жизни

1. Трек-номер посылки. Отправил посылку — получил номер вида RU123456789. Дальше и магазин, и склад, и курьер, и ты сам смотрите статус по одному и тому же номеру, хотя системы у всех разные.

Где ломается: при передаче между перевозчиками номер часто меняется — у Почты свой, у курьерской службы свой, и склейка происходит «через накладную», которую кто-то должен ввести руками. В IT это ровно та же проблема: если сервис на границе не пробросил заголовок дальше, вся цепочка рассыпается на несвязанные куски. И ещё: трек-номер говорит, где посылка, но не говорит, что внутри — так и request id сам по себе не объясняет бизнес-смысл запроса.

2. Номер заказа в большой кухне. Ты платишь на кассе, получаешь номер 47. Кассир пробил, повар приготовил, бармен налил — и всё это один заказ, потому что номер прошёл через все три точки.

Где ломается: если кассир не передал номер на кухню (а в реальности это случается — «пробей без номера»), то чек и тарелка оказываются не связаны, и разбираться приходится по времени и по памяти. В IT это забытый headers: {} при проксировании — самая частая причина, почему «трассировка не работает». Второе ограничение: один заказ легко распадается на несколько тикетов (кофе отдельно, еда отдельно), и тогда номер один, а записей много — как один trace id с несколькими span id.

3. Багажная бирка при пересадке. В аэропорту на сумку клеят бирку с номером; при пересадке её считывают, и багаж уезжает в нужный самолёт, а если потерялся — его ищут по номеру, а не по описанию «чёрный чемодан».

Где ломается: при переходе на другую авиакомпанию бирку не всегда считывают, и сумка «теряет историю». Кроме того, бирка привязана к вещи, а не к человеку: если ты летишь с двумя чемоданами, номеров два, и «мой рейс» и «мой багаж» — не одно и то же. Так и request id привязан к запросу, а не к пользователю.

Как это работает

Пошагово, на примере запроса, который проходит через три сервиса.

клиент --> edge/gateway --> service A --> service B --> БД
             |  генерирует    |  пробрасывает  |  пробрасывает
             |  request id    |  в заголовке   |  в заголовке
             v                v                v
          log: rid=7f3a..  log: rid=7f3a..  log: rid=7f3a..

Шаг 1. Решение, кто генерирует. Правило простое: генерирует первый, кто увидел запрос извне — обычно это edge-прокси, API-гейтвей или сам сервис, если он без прокси. Если клиент прислал свой идентификатор — его принимают, но с осторожностью (см. callout ниже). Внутренние сервисы никогда не генерируют новый id, они только пробрасывают полученный.

Шаг 2. Формат. Вариантов три.

Плюс почти всегда — hex-строка фиксированной длины (16 или 32 символа): её удобно резать глазами и она не содержит спецсимволов.

Шаг 3. Передача. Везде, где есть граница между процессами, нужен носитель:

Шаг 4. Логирование. В каждом сервисе request id попадает в структурированный лог — JSON-строку, а не «текст с пробелами». В большинстве фреймворков для этого есть готовый механизм: MDC (Mapped Diagnostic Context) в мире Java/slf4j, contextvars в Python, AsyncLocalStorage в Node.js, context.Context в Go. Смысл один: положить id в контекст один раз — и он автоматически появится в каждой строчке лога, включая те, что пишут библиотеки.

Шаг 5. Возврат клиенту. Идентификатор отдают обратно — и в заголовке ответа, и (обязательно!) в теле ошибки. Это то, из-за чего вообще стоит вся возня: клиент видит «внутренняя ошибка», копирует id и присылает в поддержку, а инженер за один запрос в логовой системе находит конкретную цепочку. Именно так выглядит ответ любого крупного API: поле request_id рядом с error.

Шаг 6. Трассировка (опционально, но обычно да). К request id добавляют span id — идентификатор конкретного шага — и parent span id — ссылку на шаг-родителя. Получается дерево. Формат W3C traceparent выглядит так:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
             |  |                                |                |
             |  |                                |                + флаги (сэмплировать или нет)
             |  |                                + span id (текущий шаг)
             |  + trace id (общий на всю цепочку)
             + версия формата

Шаг 7. Сэмплирование. На высоких нагрузках хранить все трассы дорого, поэтому часть отбрасывают. Есть head-based (решение принимается в начале, «сохраняем 1 из 100 запросов») и tail-based (решение принимается в конце: «сохраняем всё, что закончилось ошибкой или было медленным»). Второй вариант полезнее для отладки, но требует буферизации.

Чужой request id — это входные данные, а не факт

Если клиент может прислать свой X-Request-ID, он может прислать что угодно: строку на 10 килобайт, перевод строки (попытка log injection — подделки логов), SQL-кусок, чужой идентификатор, чтобы «подставить» другого. Всегда валидируй: только [A-Za-z0-9-], только разумная длина, иначе генерируй свой и логируй чужой отдельным полем.

Где встречается в обычной жизни

Где встречается в IT и бизнесе

Кто пользуется

Альтернативы и конкуренты

Ничего не пробрасывать, искать по времени и IP.
Плюсы: ноль работы, ничего не надо менять в коде и конфигах.
Минусы: работает только при одном сервисе и низкой нагрузке; при параллельных запросах от одного клиента вы уже не различите их; при разборе инцидента превращается в археологию.

Полноценный APM (Datadog, New Relic, Sentry, Elastic APM).
Плюсы: автоматическая инструментация, готовые дашборды, трейсы без ручной работы, алерты.
Минусы: деньги (обычно за объём данных), зависимость от вендора, «магия» — при проблеме непонятно, что именно не пробросилось; и всё равно нужно, чтобы заголовок доходил до конца.

Сразу W3C traceparent вместо своего X-Request-ID.
Плюсы: совместимость с OpenTelemetry и любым трассировщиком «из коробки»; один формат на весь зоопарк сервисов.
Минусы: длиннее и менее очевидно для человека; trace id жёстко 16 байт и не подчиняется вашим правилам; для простого «найти строчку в логе» — избыточно.

Идентификация по бизнес-ключу (order_id, user_id).
Плюсы: человекопонятно, поддержка уже умеет с этим работать, не нужна инфраструктура.
Минусы: не работает до появления сущности (запрос упал раньше); один запрос может затрагивать несколько сущностей; и это персональные данные, которые не хочется тащить в каждый лог.

Когда НЕ стоит использовать

Ценность появляется только на границах

Request id бесполезен ровно в одном месте: там, где его забыли пробросить. И это всегда граница — HTTP-клиент, вызов очереди, воркер, сторонний SDK. Поэтому в зрелых командах пишут не «не забудь передать заголовок», а интеграционный тест, который проверяет, что id дошёл до последнего сервиса в цепочке.

Связанные понятия

Литература и источники

Где встретилось у меня

Вчера утром мой собственный скрипт-генератор статей запустился три раза почти одновременно, и два запуска упали на одном и том же ответе внешнего API: HTTP 400 с телом ошибки, где рядом с текстом лежали поля вида request_id, upstream_status и code. Практическая ценность этих полей стала очевидна сразу: три параллельных запуска оставили в логах почти одинаковые строчки, отличить их друг от друга можно было только по идентификатору запроса — и именно он позволил бы провайдеру найти конкретный упавший вызов, а не «примерно тот интервал времени». Отдельно отмечу: сам вчерашний контекст был очень скудным (почти всё — повторы одного и того же сообщения об ошибке), поэтому тема выбрана по самой заметной технической сущности, которая в нём встречалась.

Краткое резюме