Короткий ответ
Доставку нужно делать асинхронно через очередь с повторными попытками и экспоненциальным backoff, подписывать каждое событие уникальным event_id для дедупликации на стороне подписчика, перемещать сообщения, исчерпавшие попытки, в DLQ с возможностью ручного/автоматического переразбора, и явно документировать подписчикам, что строгий порядок доставки между разными событиями не гарантируется, если это не было отдельно спроектировано.
Как это работает подробнее
отправка webhook — это по сути ненадёжный HTTP-вызов к стороннему сервису, который может быть недоступен, медленным или отвечать ошибкой; каждая попытка доставки должна иметь таймаут, ограниченное число повторов с нарастающей задержкой, и фиксацию результата (успех/ошибка) для наблюдаемости. Подписчик должен получать подписанный payload (HMAC) с уникальным идентификатором события, чтобы мог идемпотентно обработать повторную доставку — гарантия "ровно один раз" на уровне HTTP между двумя независимыми системами недостижима, поэтому стандарт — "как минимум один раз" плюс идемпотентность на приёмнике.
Пример кода
import time
import hmac
import hashlib
import requests
def deliver_webhook(subscriber_url: str, secret: bytes, event: dict, max_attempts: int = 5) -> bool:
body = json.dumps(event).encode()
signature = hmac.new(secret, body, hashlib.sha256).hexdigest()
for attempt in range(1, max_attempts + 1):
try:
resp = requests.post(
subscriber_url,
data=body,
headers={"Content-Type": "application/json", "X-Signature": signature, "X-Event-Id": event["event_id"]},
timeout=5,
)
if resp.status_code < 300:
return True
except requests.exceptions.RequestException:
pass
time.sleep(min(2 ** attempt, 60))
return False # после max_attempts — сообщение уходит в DLQ для ручного/отложенного переразбораЧто использовать на практике
- Рекомендация. нужно решить, сохранять ли строгий порядок доставки событий одного подписчика (например, через последовательную обработку на партицию по ID получателя) или допускать параллельную доставку разных событий с возможным нарушением порядка ради меньшей задержки и большей throughput — для событий типа "статус заказа изменился" порядок может быть критичен, для "новый лог записан" — не обязательно. DLQ должен быть снабжён алертингом (чтобы команда знала о накоплении недоставленных сообщений) и инструментом ручного replay после восстановления подписчика. Компромисс: гарантия строгого порядка доставки (например, через партиционирование очереди по получателю и последовательную обработку) снижает throughput и усложняет масштабирование обработки по сравнению с полностью параллельной доставкой, но устраняет риски бизнес-ошибок из-за рассинхронизации порядка событий у подписчика; неограниченные повторные попытки снижают потерю сообщений, но могут надолго занимать ресурсы на недоступного подписчика и задерживать видимость проблемы, тогда как агрессивный DLQ с малым числом попыток быстрее освобождает ресурсы, но требует механизма ручного/автоматического переразбора и мониторинга накопления в DLQ. Конкретный выбор числа попыток, необходимости упорядочивания и SLA по задержке доставки зависит от бизнес-критичности события для конкретных подписчиков и договорённостей с ними.
Подводные камни
- Риски. делать синхронную доставку webhook внутри обработки основного бизнес-события (без очереди), из-за чего недоступность одного подписчика замедляет или ломает основной поток обработки события; не предусматривать дедупликацию на стороне подписчика через
event_id, полагаясь, что "обычно" повторов не бывает; игнорировать упорядочивание там, где оно критично для бизнес-логики подписчика (например, доставить "заказ отменён" раньше "заказ создан" из-за параллельной обработки).