В статье разберём, почему timeout не позволяет однозначно определить результат бизнес‑операции и как безопасно обрабатывать такие ситуации.
Представим ситуацию:
Наша система отправляет внешней системе запрос «Создать платёж на 50 000 ₽»
POST /paymentsВнешняя система получает запрос и создаёт платёж
Внешняя система отправляет ответ о созданном платеже
При отправке ответа происходит ошибка и он не доходит до нашей системы
Наша система не получает ответ в установленное время и фиксирует timeout
Можно ли повторить запрос на создание платежа?
Коротко: ответ зависит от того, создался ли в итоге платеж во внешней системе то есть от результата первого вызова.
Но как узнать результат выполнения первого вызова, если в ответе мы получили только timeout?
Подсказка
Спросите это у внешней системы
В статье мы подробно рассмотрим как это можно сделать.
Начнем пожалуй с рассмотрения какие вообще могут быть ответы на запрос.
Три возможных результата интеграционного вызова
С точки зрения вызывающей системы результат операции можно условно отнести к одной из трёх категорий: SUCCESS, CONFIRMED FAILURE и UNKNOWN.
SUCCESS — подтверждённый успех
Внешняя система вернула успешный ответ, из которого мы понимаем, что операция выполнена. Например, получили 201 Created и идентификатор созданного платежа.
В такой ситуации всё достаточно очевидно: сохраняем результат и продолжаем бизнес‑процесс.
CONFIRMED FAILURE — подтверждённая ошибка
В этом случае мы также достоверно знаем результат операции, но он отрицательный: операция не выполнена.
Например, внешний API вернул 400 Bad Request, потому что запрос не соответствует контракту или содержит некорректные данные. Повтор того же запроса без изменения его содержимого проблему не решит: сначала необходимо устранить причину ошибки, а уже затем выполнять новую попытку.
При этом важно не привязывать бизнес‑логику только к конкретному HTTP‑коду. Ключевой вопрос для нас — можем ли мы согласно контракту внешнего API однозначно определить, что бизнес‑операция не была выполнена.
UNKNOWN — результат неизвестен
Именно здесь становится интереснее. Timeout может привести к ситуации, когда результат операции остаётся неизвестным.
Наша система отправила запрос, но не получила ответа и поэтому не может однозначно определить, успела ли внешняя система выполнить операцию. Возможны два внешне одинаковых для нас сценария: запрос вообще не был обработан и платёж не создан либо запрос был обработан, платёж создан, но ответ потерялся по дороге обратно.
Для нашей системы результат один — ответа нет.
Получается важное различие: технический результат вызова и бизнес‑результат операции — не одно и то же. Поэтому считать timeout подтверждением того, что операция не выполнилась, нельзя.
Как узнать результат первого запроса?
Для этого интеграция должна позволять идентифицировать именно первоначальную операцию.
Например, при создании платежа наша система может заранее сформировать уникальный operation_id = 7f8... и передать его во внешнюю систему. После timeout вместо создания новой операции мы обращаемся к методу проверки статуса:
GET /payments/operations/{operation_id}
В ответ внешний сервис может вернуть, например, SUCCESS, FAILED или PROCESSING.
Такую проверку и последующую сверку состояния операции между взаимодействующими системами можно рассматривать как reconciliation — восстановление достоверного состояния операции.
Если получен SUCCESS, система продолжает бизнес‑процесс. При FAILED она обрабатывает подтверждённую ошибку. Если операция всё ещё находится в PROCESSING либо окончательный результат пока невозможно установить, система продолжает проверку согласно предусмотренной политике или переводит операцию в отдельный сценарий обработки.
Главное здесь — не угадать результат первоначального вызова, а восстановить его достоверное состояние.
А нельзя просто сделать retry?
Иногда можно, но сначала нужно понять, безопасен ли повтор.
Представим внешний API, который при каждом вызове POST /payments создаёт новый платёж. Первый запрос успешно обрабатывается, появляется платёж № 1, но наша система получает timeout. Если после этого просто повторить POST /payments, внешний сервис может создать платёж № 2.
Получается, что механизм, который должен был повысить надёжность интеграции, вместо этого создал две бизнес‑операции.
Именно здесь появляется понятие идемпотентности.
Как сделать повтор безопасным: Idempotency‑Key
Допустим, внешний API поддерживает Idempotency-Key. Перед первым запросом наша система генерирует уникальный ключ, например:
Idempotency-Key: ABC
и передаёт его вместе с POST /payments.
Смысл такого ключа для внешней системы можно сформулировать так: это конкретная бизнес‑операция, и повтор запроса с тем же ключом не должен восприниматься как новая операция.
Предположим, внешний сервис успешно создал payment_id = 123, но ответ снова потерялся. Наша система получает timeout и решает выполнить retry. В этом случае новый Idempotency-Key генерировать не нужно — запрос повторяется с тем же ABC.
Внешняя система видит, что операция с таким ключом уже обрабатывалась, и в зависимости от контракта API может вернуть результат первоначальной операции либо продолжить обработку уже существующей.
Если же отправить новый ключ, например XYZ, внешний сервис вправе воспринять запрос как создание новой бизнес‑операции.
Отсюда следует важное правило:
Одна бизнес‑операция — один Idempotency‑Key, который сохраняется при её повторных попытках.
При этом конкретные гарантии всегда зависят от реализации внешнего API: аналитику важно выяснить, как долго хранится ключ, как определяется его уникальность и что именно сервис вернёт при повторном запросе.
Что лучше: reconciliation или retry с Idempotency‑Key?
Эти механизмы не обязательно конкурируют друг с другом, потому что решают разные задачи.
Reconciliation отвечает на вопрос: «Что произошло с уже отправленной операцией?» Retry — на вопрос: «Можно ли безопасно попробовать выполнить её ещё раз?»
После timeout система может сначала проверить состояние первоначальной операции, а если результат всё ещё неизвестен и контракт разрешает безопасный повтор — выполнить retry с тем же Idempotency-Key. После этого при необходимости состояние операции можно проверить повторно.
Задача надёжной интеграции состоит не в выборе одного универсального механизма, а в понимании того, какую проблему решает каждый из них.
Какие ошибки вообще стоит ретраить?
Retry полезен далеко не для любой ошибки.
Если внешний сервис вернул бизнес‑ошибку или ошибку валидации и причина не изменится без изменения самого запроса, повторять его бессмысленно. Совсем другая ситуация — временная техническая недоступность: timeout, ошибка соединения, 502 Bad Gateway, 503 Service Unavailable, 429 Too Many Requests или 504 Gateway Timeout.
Такие проблемы потенциально могут исчезнуть самостоятельно, поэтому retry здесь уже может иметь смысл. Однако решение о повторе нельзя принимать только на основании HTTP‑кода. Необходимо учитывать контракт внешнего API, идемпотентность операции и возможность того, что бизнес‑операция уже частично или полностью выполнилась.
Возникает следующий вопрос: как часто выполнять повторные запросы?
Если внешний сервис временно недоступен, бессмысленно отправлять новый запрос каждую секунду. Так наша система не только не решит проблему, но и создаст дополнительную нагрузку на уже испытывающий трудности сервис.
Поэтому между повторными попытками обычно увеличивают интервал ожидания. Например:
1 секунда → 2 секунды → 4 секунды → 8 секунд → 16 секунд
Такой подход называется backoff. Его задача — уменьшить частоту повторов и дать внешнему сервису время восстановиться.
А зачем нужен jitter?
Представим теперь, что внешний сервис используется тысячами клиентов. Он стал недоступен, и все они примерно одновременно получили 503.
Если каждый клиент использует одинаковый backoff, через одну секунду тысячи запросов снова отправятся одновременно. Через две секунды ситуация повторится. Сервис может только начать восстанавливаться — и сразу получить очередной всплеск нагрузки.
Чтобы этого избежать, к интервалу ожидания добавляют небольшую случайную составляющую — jitter. Например, вместо того чтобы все клиенты повторили запрос ровно через четыре секунды, один сделает это через 3,7 секунды, другой через 4,2, третий через 4,8.
Таким образом повторные запросы распределяются во времени.
Backoff уменьшает частоту повторов, а jitter снижает вероятность того, что большое количество клиентов повторит запрос одновременно.
А сколько retry делать?
Универсального числа попыток не существует. Оно зависит от критичности операции, SLA внешнего сервиса, допустимого времени ожидания, характера ошибки, возможностей reconciliation и требований конкретного бизнес‑процесса.
Важно другое: система не должна выполнять повторные запросы бесконечно и незаметно для эксплуатации.
Если операция слишком долго остаётся в проблемном состоянии, необходимо сделать это состояние наблюдаемым: сохранять количество попыток, длительность проблемы и причину последней ошибки. При достижении установленного порога можно сформировать alert, продолжить автоматическую сверку либо передать операцию на ручной разбор.
То есть retry не должен существовать сам по себе — для него также нужны правила наблюдаемости и обработки ситуации, когда автоматическое восстановление не помогает.
Retry и UNKNOWN — не одно и то же
Эти понятия важно разделять.
Если мы получили временную техническую ошибку и контракт гарантирует, что операция не была выполнена либо разрешает безопасный повтор, retry может быть обычным механизмом восстановления.
Но если после timeout мы не знаем, выполнилась ли первоначальная операция, её результат становится UNKNOWN. Слепой retry в такой ситуации может привести к дублю, если операция не является идемпотентной.
Поэтому сначала нужно ответить на вопрос: можем ли мы безопасно повторить именно эту бизнес‑операцию?
Если да — например, благодаря Idempotency-Key — retry допустим. Если нет, безопаснее сначала выполнить reconciliation и попытаться установить состояние первоначальной операции.
Что должен предусмотреть системный аналитик при описании интеграции?
Спойлер
happy path и список HTTP‑кодов — это ещё не полное описание интеграции.
Перед передачей задачи в разработку аналитику важно определить, какой бизнес‑результат считается подтверждённым успехом и какие ответы означают подтверждённую ошибку. Также необходимо понять, в каких ситуациях результат операции может стать UNKNOWN и каким образом система сможет восстановить её состояние: через operation_id, status API, бизнес‑ключ или другой механизм.
Отдельно нужно определить правила безопасного повторения запросов. Поддерживает ли внешний сервис Idempotency-Key? Кто его генерирует? Как долго он хранится? Нужно ли использовать тот же ключ при retry?
Также необходимо заранее описать поведение системы при длительной недоступности: допустимое количество попыток, правила backoff и jitter, условия формирования alert, необходимость ручного разбора и допустимое время ожидания.
И ещё один важный вопрос — что увидит пользователь?
После timeout нельзя безусловно показывать сообщение «Платёж не создан», потому что система этого не знает. Гораздо корректнее использовать формулировку вроде:
«Результат операции уточняется».
Тем самым пользовательский статус отражает реальное состояние системы, а не предположение о результате внешней операции.
Чек‑лист аналитика перед согласованием интеграции
Что выяснить | Пример вопроса |
|---|---|
Контракт API | Есть ли OpenAPI/Swagger? Описаны ли поля, обязательность, форматы и enum? |
Идентификация операции | Какой |
Идемпотентность | Поддерживается ли |
Авторизация | OAuth2, mTLS, API Key? Как обновляются токены? |
Ограничения | Есть ли rate limit, ограничения размера и частоты запросов? |
Timeout | Какое максимальное время ответа? Может ли операция продолжить выполняться после timeout? |
Ошибки | Какие ответы гарантируют, что операция не выполнена, а какие оставляют результат |
Retry | Какие ошибки разрешено повторять? Каковы правила backoff и максимальное число попыток? |
Reconciliation | Есть ли status API или другой способ проверить результат первоначальной операции? |
Дубли | Что произойдёт при повторном |
Асинхронность | Операция завершается сразу или может перейти в |
Получение результата | Используется polling, webhook/callback или событие? |
SLA | За какое время внешний сервис должен обработать операцию? |
Наблюдаемость | Есть ли |
Тестирование | Есть ли тестовый контур и примеры успешных и ошибочных сценариев? |
Вместо вывода
Главная проблема timeout заключается не в том, что система не получила ответ, а в том, что после этого мы можем не знать результат самой бизнес‑операции.
Поэтому при проектировании интеграции важно разделять три ситуации: подтверждённый успех, подтверждённую ошибку и неизвестный результат. Для каждой из них должно быть определено своё поведение системы.
Retry помогает повторить временно неудавшуюся операцию. Idempotency-Key позволяет сделать такой повтор безопаснее. Reconciliation помогает выяснить, что произошло с уже отправленной операцией. Backoff и jitter не дают механизму повторов самому стать причиной дополнительной нагрузки.
Для системного аналитика из этого следует простой вывод: описать endpoint, request/response и HTTP‑коды недостаточно. Хороший интеграционный сценарий должен отвечать ещё на один вопрос:
Что будет делать система, если она отправила запрос, но так и не поняла, что произошло?

