# Доставка и надёжность webhook: повторы, порядок, dead-letter

> Webhook — это HTTP-callback от сервера к серверу: платформа сообщений выполняет POST события на URL потребителя. Надёжная доставка работает по принципу at-least-once, поэтому производители повторяют неудавшиеся доставки с экспоненциальной выдержкой и джиттером, отправляют в dead-letter после лимита, а потребители обязаны быть идемпотентными, поскольку дубликаты и события вне порядка ожидаемы.

- Канонический URL: https://www.broadnet.me/ru/reference/webhook-delivery-and-reliability
- Язык: ru
- Последнее обновление: 2026-07-19
- Издатель: BroadNet (https://www.broadnet.me)

Стандартная архитектура webhook/callback и её модель надёжности: доставка at-least-once, экспоненциальная выдержка с джиттером при повторах, тайм-ауты доставки, гарантии порядка, очереди dead-letter, идемпотентные потребители и здоровье эндпоинта / размыкание цепи для защиты отказавшего приёмника.

## Архитектура webhook

Webhook меняет местами обычные роли клиента и сервера: вместо того чтобы приложение опрашивало платформу сообщений на предмет обновлений, платформа выполняет POST события на URL, который зарегистрировало приложение, — изменение статуса доставки, входящее сообщение, событие аккаунта. Это push-половина асинхронной модели, pull-половиной которой является опрос GET /messages/{id}. Эндпоинт потребителя — это обычный HTTPS-сервер, который должен принять POST, сделать минимум, необходимый для надёжной записи события, и быстро вернуть 2xx. Распространённый и важный паттерн — подтвердить приём и обработать асинхронно: эндпоинт записывает сырое событие в очередь или журнал, возвращает 200, а отдельный воркер выполняет реальную работу — так медленная бизнес-логика никогда не заставит производителя выйти за тайм-аут и повторить.

*POST webhook о статусе доставки потребителю*

```http
POST /webhooks/dlr HTTP/1.1
Host: app.example
Content-Type: application/json
Webhook-Id: 8b1a...
Webhook-Timestamp: 1752916522
Webhook-Signature: v1,3f2b...

{
  "event": "message.delivered",
  "message_id": "01HZY8Q3M6",
  "status": "delivered",
  "occurred_at": "2026-07-19T09:15:25Z"
}
```

## Гарантии доставки: at-least-once

Распределённая доставка не может быть одновременно exactly-once и доступной, поэтому почти каждая продакшн-система webhook выбирает at-least-once: производитель повторяет попытки, пока не увидит подтверждение об успехе, принимая, что потребитель иногда может получить одно и то же событие более одного раза. Альтернативные крайности хуже — at-most-once теряет события при первом сбое, а истинный exactly-once требует распределённой координации, которую транспорт HTTP не предоставляет. At-least-once возлагает одну обязанность на потребителя: он должен быть идемпотентным, дедуплицируя по стабильному идентификатору события, чтобы повторно доставленное событие было безвредным. Контракт подтверждения прост: любой 2xx означает «прекратить повторы»; всё остальное (или тайм-аут) означает «повторить позже».

## Повторы с экспоненциальной выдержкой и джиттером

Когда доставка не удаётся или истекает по тайм-ауту, производитель повторяет по расписанию, растущему экспоненциально, — базовая задержка удваивается на каждой попытке до потолка, — чтобы кратковременно недоступный эндпоинт не забрасывали. Важно, что задержка включает случайный джиттер: без него множество событий, отказавших во время одного и того же сбоя, повторяли бы попытки в унисон и создали бы синхронизированное «стадо» в момент восстановления эндпоинта. Полный джиттер (случайное значение между нулём и текущей границей выдержки) равномерно распределяет эту нагрузку. Повторы продолжаются, пока событие не будет успешным или не будет достигнут общий лимит попыток либо времени, после чего событие отправляется в dead-letter, а не повторяется вечно.

*Экспоненциальная выдержка с полным джиттером*

```text
base = 2s ; factor = 2 ; cap = 3600s
attempt n bound = min(cap, base * factor^(n-1))
delay(n)        = random(0, bound)      # full jitter

n=1  bound  2s    delay in [0, 2s]
n=2  bound  4s    delay in [0, 4s]
n=3  bound  8s    delay in [0, 8s]
n=4  bound 16s    delay in [0, 16s]
...  until success or attempt/time cap -> dead-letter
```

## Тайм-ауты и порядок

Производитель устанавливает короткий тайм-аут на каждую доставку — обычно несколько секунд — и трактует тайм-аут так же, как сбой: соединение разрывается, а событие ставится в план на повтор. Это механическая причина, по которой потребителям следует подтверждать быстро и обрабатывать асинхронно. Порядок — более сложная гарантия. Поскольку повторы и параллельная доставка чередуются, наивный поток webhook упорядочен лишь по возможности: повторённое более раннее событие может прийти после более позднего. Системы, которым нужен порядок, либо доставляют по ключу последовательно (все события для одного message id на одной упорядоченной партиции, с блокировкой при сбое), либо, что более распространено, делают полезную нагрузку самоупорядоченной — каждое событие несёт метку времени и монотонную последовательность, чтобы потребитель мог отбросить устаревшее обновление, а не зависеть от порядка прибытия.

*Стратегии упорядочивания потоков webhook*

| Стратегия | Как это работает | Стоимость |
|---|---|---|
| Best-effort | Доставлять по мере готовности, повторять независимо | События могут приходить не по порядку |
| Упорядочение по ключу | Последовательно обрабатывать все события для одного идентификатора; блокировать выполнение при сбое | Блокировка начала очереди (head-of-line), снижение пропускной способности |
| Самоупорядочивающаяся полезная нагрузка | Передаются метка времени + порядковый номер; потребитель отбрасывает устаревшие | Логика на стороне потребителя, но без блокировки |

## Dead-letter и здоровье эндпоинта

Эндпоинт, остающийся недоступным, не должен вечно потреблять ёмкость повторов. После исчерпания политики повторов событие перемещается в очередь dead-letter — надёжное хранилище недоставленных событий, которое оператор может изучить и воспроизвести после починки эндпоинта, — а не молча отбрасывается. Чтобы защитить обе стороны, пока эндпоинт нездоров, производители отслеживают здоровье доставки по каждому эндпоинту и применяют размыкание цепи: после устойчивой череды сбоев цепь размыкается, и новые события паркуются или быстро отклоняются вместо попыток, а затем полуоткрытая проба периодически проверяет восстановление, прежде чем возобновится нормальная доставка. Это не даёт мёртвому потребителю порождать очередь обречённых попыток и позволяет восстанавливающемуся плавно набрать обороты. Конкретное поведение webhook и повторов BroadNet задокументировано для разработчиков.

## Построение идемпотентного потребителя

Поскольку доставка at-least-once гарантирует дубликаты, эффекты exactly-once принадлежат потребителю, а не производителю. Паттерн — хранилище дедупликации по стабильному, предоставленному производителем идентификатору события (Webhook-Id или event id, никогда не только по хешу полезной нагрузки). При приёме потребитель проверяет, обрабатывал ли он уже этот id; если да — подтверждает 2xx и больше ничего не делает; если нет — записывает id и применяет эффект в той же транзакции, чтобы сбой не смог применить эффект без записи id. В сочетании с самоупорядоченными полезными нагрузками это превращает повторно доставленное или пришедшее вне порядка событие в no-op, что и делает из транспорта at-least-once бизнес-результаты exactly-once.

*Идемпотентный потребитель (псевдокод)*

```text
on POST(event):
  verify_signature(event)          # reject if invalid
  id = event.headers['Webhook-Id']
  begin transaction:
    if seen(id): return 200        # duplicate -> no-op
    if event.sequence <= last_seq(event.message_id):
        mark_seen(id); return 200  # stale -> discard
    apply_effect(event)
    mark_seen(id); set_last_seq(...)
  commit
  return 200
```

## Частые вопросы

### Какую гарантию доставки дают webhook?

At-least-once. Производитель повторяет, пока не увидит подтверждение 2xx, поэтому потребитель может получить одно и то же событие более одного раза. Exactly-once поверх обычного HTTP потребовал бы распределённой координации, которую транспорт не предоставляет, поэтому от потребителя ожидается идемпотентность и дедупликация по стабильному event id.

### Зачем добавлять джиттер в выдержку повторов webhook?

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

### Как должен отвечать эндпоинт webhook?

Быстро и кодом 2xx. Рекомендуемый паттерн — надёжно записать сырое событие, немедленно вернуть 200 и обработать асинхронно в отдельном воркере. Так медленная бизнес-логика никогда не приведёт к срабатыванию тайм-аута доставки производителя и ненужному повтору.

### Доставляются ли события webhook по порядку?

По умолчанию только по возможности — повторы и параллельная доставка могут чередоваться, поэтому более раннее событие может прийти после более позднего. Чтобы получить порядок, либо сериализуйте по ключу (ценой блокировки начала очереди), либо сделайте полезную нагрузку самоупорядоченной с меткой времени и номером последовательности, чтобы потребитель отбрасывал устаревшие обновления.

### Что такое очередь dead-letter для webhook?

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

### Зачем применять размыкание цепи к эндпоинтам webhook?

Иначе стабильно отказывающий эндпоинт потреблял бы ёмкость повторов обречёнными попытками и накапливал бы очередь. Размыкание цепи срабатывает после устойчивых сбоев, паркуя или быстро отклоняя новые события, а затем использует полуоткрытую пробу для проверки восстановления перед возобновлением нормальной доставки — защищая и производителя, и восстанавливающегося потребителя.

## Источники

- [IETF RFC 9110 — HTTP Semantics (method properties, 202)](https://datatracker.ietf.org/doc/html/rfc9110)
- [IETF draft — Idempotency-Key HTTP Header Field](https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header)

## См. также

- [Проверка подписи webhook](https://www.broadnet.me/ru/reference/webhook-signature-verification)
- [Webhook статуса сообщения и входящих (MO)](https://www.broadnet.me/ru/reference/message-status-and-inbound-webhooks)
- [Идемпотентность и ограничение скорости](https://www.broadnet.me/ru/reference/idempotency-and-rate-limiting)
- [Руководство по webhook BroadNet](https://www.broadnet.me/ru/developers/webhooks)
- [Повторы и выдержка BroadNet](https://www.broadnet.me/developers/retries)
