Webhooks: повторы и квитирование
1) Базовая модель доставки
At-least-once (по умолчанию): событие будет доставлено ≥1 раз. Гарантии ровно-один-раз достигаются идемпотентностью приемника.
Квитирование (ACK): только любая 2xx (обычно 200/204) от получателя означает успех. Все остальное трактуется как отказ и ведет к повтору.
Быстрый ACK: отвечайте 2xx после помещения события в свою очередь, а не после полной бизнес-обработки.
2) Формат событий и обязательные заголовки
Полезная нагрузка (пример)
json
{
"id": "evt_01HXYZ",
"type": "order. created",
"occurred_at": "2025-11-03T18:10:12Z",
"sequence": 128374,
"source": "orders",
"data": { "order_id": "o_123", "amount": "49. 90", "currency": "EUR" },
"schema_version": 1
}
Заголовки отправителя
`X-Webhook-Id: evt_01HXYZ` — уникальный ID события (используйте для дедупликации).
`X-Webhook-Seq: 128374` — монотонная последовательность (по подписке/теме).
`X-Signature: sha256=<base64(hmac_sha256(body, secret))>` — HMAC-подпись.
`X-Retry: 0,1,2...` — номер попытки.
`X-Webhook-Version: 1` — версионирование контракта.
(опционально) `Traceparent` — корреляция трасс.
Ответ получателя
2xx — успешно принято (дальше повторов по этому `id` не будет).
410 Gone — endpoint удален/неактивен → отправитель прекращает повторы и деактивирует подписку.
429/5xx/таймаут — отправитель повторяет по политике ретраев.
3) Политика повторов (retries)
Рекомендованная лестница backoff (+ jitter)
`1s, 3s, 10s, 30s, 2m, 10m, 30m, 2h, 6h, 24h` (останавливаемся после лимита, например 48–72 часов).
Правила:- Экспоненциальный backoff + случайный jitter (±20–30%) для избегания «стадного эффекта».
- Кворум ошибок для временных сбоев (например, повтор, если 5xx или таймаут сети).
- Respect 429: ставьте минимум `min(заголовок Retry-After, следующее окно backoff)`.
Таймауты и размеры
Таймаут соединения ≤ 3–5 сек; общий таймаут ответа ≤ 10 сек.
Размер тела по контракту (например, ≤ 256 KB), иначе 413 → логика «chunking» или «pull URL».
4) Идемпотентность и дедупликация
Идемпотентное применение: обработка повторов того же `id` должна возвращать тот же результат и не менять состояние повторно.
Дедуп-хранилище на стороне получателя: хранить `(X-Webhook-Id, processed_at, checksum)` с TTL ≥ окна ретраев (24–72 ч).
Композиционный ключ: если несколько топиков → `(subscription_id, event_id)`.
5) Порядок и «exactly-once эффекты»
Гарантировать строгий порядок сложно в распределенных системах. Используйте:- Partition by key: одно и то же логическое множество (например, `order_id`) всегда в один «канал» доставки.
- Sequence: отклоняйте события со старым `X-Webhook-Seq` и ставьте их в «parking lot» до прихода недостающих.
- журнал примененных операций (outbox/inbox pattern),
- транзакционное upsert по `event_id` в БД,
- саги/компенсации для сложных процессов.
6) Решение ошибок по статус-кодам (таблица)
7) Безопасность канала
Подпись HMAC каждого сообщения; проверка на приемнике с «окном времени» (mitm и replay-атаки).
mTLS для чувствительных доменов (KYC/платежи).
IP allowlist исходящих адресов, TLS 1.2+, HSTS.
PII-минимизация: не отправляйте лишние персональные данные; маскируйте в логах.
Ротация секретов: два действующих ключа (active/next) и заголовок `X-Key-Id` для указания текущего.
8) Очереди, DLQ и реплей
События обязательно пишутся в выходную очередь/журнал на стороне отправителя (для надежного реплея).
При превышении максимума ретраев — событие уходит в DLQ (Dead Letter Queue) с причиной.
Replay API (для получателя/оператора): повторная отправка по `id`/диапазону времени/теме, с ограничением RPS и дополнительной подписью/авторизацией.
POST /v1/webhooks/replay
{ "subscription_id": "sub_123", "from": "2025-11-03T00:00:00Z", "to": "2025-11-03T12:00:00Z" }
→ 202 Accepted
9) Контракт и версия
Версионируйте событие (поле `schema_version`) и транспорт (`X-Webhook-Version`).
Добавляйте поля только как опциональные; при удалении — минорная миграция и переходный период (dual-write).
Документируйте типы событий, примеры, схемы (JSON Schema), коды ошибок.
10) Наблюдаемость и SLO
Ключевые метрики отправителя:- `delivery_success_rate` (2xx/все попытки), `first_attempt_success_rate`
- `retries_total`, `max_retry_age_seconds`, `dlq_count`
- `latency_p50/p95` (occurred_at → ack_received_at)
- `ack_latency` (receive → 2xx), `processing_latency` (enqueue → done)
- `duplicates_total`, `invalid_signature_total`, `out_of_order_total`
99.9% событий получают первый ACK ≤ 60 сек (28d).
- DLQ ≤ 0.1% от общего числа; реплей DLQ ≤ 24 ч.
11) Тайминг и разрывы сети
Используйте UTC в полях времени; синхронизируйте NTP.
Отправляйте `occurred_at` и фиксируйте `delivered_at`, чтобы считать лаг.
При длительных разрывах сеть/endpoint → накапливайте в очереди, ограничивайте рост (backpressure + квоты).
12) Рекомендованные лимиты и гигиена
RPS на подписку (например, 50 RPS, burst 100) + параллелизм (например, 10).
Макс. тело: 64–256 KB; для большего — «notification + URL» и подпись на скачивание.
Имена событий в `snake.case` или `dot.type` (`order.created`).
Строгая идемпотентность write-операций приемника.
13) Примеры: отправитель и получатель
13.1 Отправитель (псевдокод)
python def send_event(event, attempt=0):
body = json. dumps(event)
sig = hmac_sha256_base64(body, secret)
headers = {
"X-Webhook-Id": event["id"],
"X-Webhook-Seq": str(event["sequence"]),
"X-Retry": str(attempt),
"X-Signature": f"sha256={sig}",
"Content-Type": "application/json"
}
res = http. post(endpoint, body, headers, timeout=10)
if 200 <= res. status < 300:
mark_delivered(event["id"])
elif res. status == 410:
deactivate_subscription()
else:
schedule_retry(event, attempt+1) # backoff + jitter, respect 429 Retry-After
13.2 Получатель (псевдокод)
python
@app. post("/webhooks")
def handle():
body = request. data headers = request. headers assert verify_hmac(body, headers["X-Signature"], secret)
evt_id = headers["X-Webhook-Id"]
if dedup_store. exists(evt_id):
return, "" 204 enqueue_for_processing (body) # fast path. dedup_store put(evt_id, ttl=723600)
return, "" 202 # or 204
14) Тестирование и хаос-практики
Негативные кейсы: невалидная подпись, 429/5xx, таймаут, 410, большие payload’ы.
Поведенческие: out-of-order, дубликаты, задержки 1–10 минут, разрыв на 24 часа.
Нагрузочные: burst 10×; проверьте backpressure и устойчивость DLQ.
Контракты: JSON Schema, обязательные заголовки, стабильные типы событий.
15) Чек-лист внедрения
- 2xx = ACK, и быстрый возврат после enqueue
- Экспоненциальный backoff + jitter, уважение `Retry-After`
- Идемпотентность приемника и дедуп по `X-Webhook-Id` (TTL ≥ ретраи)
- Подписи HMAC, ротация секретов, optional mTLS
- DLQ + Replay API, мониторинг и алерты
- Ограничения: таймауты, RPS, размер тела
- Порядок: partition by key или `sequence` + «parking lot»
- Документация: схемы, примеры, кодовки ошибок, версии
- Хаос-тесты: задержки, дубли, отказ сети, длительный replay
16) Мини-FAQ
Нужно ли всегда отвечать 200?
Любая 2xx засчитывается как успех. 202/204 — нормальная практика для «принято в очередь».
Можно ли остановить повторы?
Да, ответом 410 и/или через консоль/API отправителя (отключение подписки).
Как быть с большими payload’ами?
Отправляйте «уведомление + secure URL», подпишите запрос на скачивание и установите TTL.
Как обеспечить порядок?
Partition by key + `sequence`; при расхождении — «parking lot» и переигрывание.
Итог
Надежные вебхуки — это четкая семантика ACK (2xx), разумные повторы с backoff+jitter, строгая идемпотентность и дедупликация, грамотная безопасность (HMAC/mTLS), очереди + DLQ + реплей, и прозрачная наблюдаемость. Зафиксируйте контракт, введите лимиты и метрики, регулярно прогоняйте хаос-сценарии — и ваши интеграции перестанут «сыпаться» при первых же сбоях.