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/5хх/таймаут - відправник повторює по політиці ретраїв.
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 для чутливих доменів (КУС/платежі).
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'( 2хх/всі спроби),'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 + реплей, і прозора спостережуваність. Зафіксуйте контракт, введіть ліміти і метрики, регулярно проганяйте хаос-сценарії - і ваші інтеграції перестануть «сипатися» при перших же збоях.