Real-Time Backend
Webhook Delivery
Платёжный провайдер отправил webhook 'payment.failed'. Endpoint вернул 200 OK, но код упал на следующей строке после ответа. Пользователь не получил уведомление об отказе платежа. Retry не было - провайдер решил что доставка прошла. Через неделю клиент звонит: 'почему со счёта списали деньги но заказ не создали?' Без DLQ и delivery log - этот инцидент нераследуем.
- **Stripe:** Retry до 3 дней (5 попыток), Dashboard с полным delivery log включая response body. At-least-once семантика - документально подтверждена. Рекомендует проверять event_id для идемпотентности.
- **Svix (webhook infrastructure):** используется Clerk, Resend, Loops. 90-дневный audit trail каждой попытки. Auto-disable endpoint при >50% failure rate. Bulk replay через API - лидер в webhook-as-a-service.
- **AWS SQS + Lambda:** стандартный паттерн DLQ в AWS. Lambda автоматически помещает в DLQ сообщения после maxReceiveCount попыток. CloudWatch Metrics дают delivery success rate из коробки.
- **Shopify:** merchant-facing dashboard с историей webhook deliveries 5 дней. Кнопка ручного retry прямо в UI. Снижение обращений в поддержку на 40% по внутренним данным после добавления этого UI.
At-least-once Delivery
At-least-once delivery - это гарантия: событие будет доставлено получателю хотя бы один раз, даже если первая попытка не удалась. В противоположность exactly-once, которая практически недостижима в распределённых системах из-за проблемы двух генералов. At-least-once - разумный компромисс, при условии что получатель идемпотентен.
Классическая проблема: сервер отправил webhook, получатель обработал запрос и сделал все действия, но при отправке ответа 200 OK соединение разорвалось. Отправитель не получил подтверждения - он решит что доставка не удалась и повторит. Получатель получит тот же webhook повторно. Без идемпотентности - дубль в БД, двойное списание, двойное письмо.
Идемпотентность получателя реализуется через event_id: перед обработкой проверить не был ли этот event_id уже обработан (Redis SET NX или уникальный индекс в БД). Если был - вернуть 200 OK без повторной обработки. Хранить обработанные ID нужно достаточно долго: Stripe ретраит до 3 дней, значит TTL должен быть не менее 72 часов.
Webhook-получатель обработал событие (списал деньги, отправил письмо), но соединение разорвалось до отправки 200 OK. Отправитель повторит доставку. Как предотвратить двойное списание?
Ordering событий
Webhooks не гарантируют порядок доставки. Событие 'order.updated' может прийти раньше 'order.created' из-за сетевых задержек, параллельных retry-очередей, разных воркеров. Получатель должен либо обрабатывать события идемпотентно вне зависимости от порядка, либо восстанавливать порядок самостоятельно.
Stripe явно документирует: 'Event delivery order is not guaranteed'. Их рекомендация - использовать sequence числа внутри одного объекта или всегда делать re-fetch через API после получения webhook. Re-fetch гарантирует актуальные данные независимо от порядка событий, но создаёт дополнительные запросы к API.
Получены два webhook от Stripe: 'payment_intent.processing' с sequence=2 и 'payment_intent.succeeded' с sequence=3. Сначала пришёл sequence=3. Что делать?
Dead Letter Queue для webhook
Dead Letter Queue (DLQ) - хранилище для событий, которые не удалось доставить после всех retry. Это не 'мусорная корзина' - это инструмент операционной видимости и ручного восстановления. Без DLQ потерянные события исчезают бесследно, и команда узнаёт о проблеме только от клиентов.
AWS SQS DLQ автоматически принимает сообщения после N неудачных попыток обработки. Shopify отображает DLQ в merchant dashboard как 'Failed deliveries' с кнопкой ручного retry. Это снижает нагрузку на поддержку: мерчант сам видит что пошло не так и когда его endpoint вернул в строй - нажимает retry самостоятельно.
Endpoint клиента был недоступен 6 часов из-за деплоя. За это время в DLQ накопилось 10 000 событий. Endpoint восстановлен. Как сделать replay?
Webhook Dashboard и Observability
Webhook dashboard - это операционный интерфейс для мониторинга доставки событий. Без него webhook-интеграция - чёрный ящик: клиент не знает получает ли он события, сколько потеряно, какие endpoint-ы проблемные. Dashboard превращает invisible delivery в наблюдаемый процесс.
- **Delivery log** - история всех попыток: timestamp, status code, response time, payload. Минимум последние 72 часа (полное retry окно)
- **Success rate graph** - процент успешных доставок в разрезе endpoint-ов и типов событий
- **Dead letter list** - события в DLQ с кнопкой ручного replay и фильтрами по типу/времени
- **Latency percentiles** - p50/p95/p99 времени доставки. Outliers указывают на проблемы с конкретным endpoint-ом
- **Endpoint health** - автоматическая пометка проблемных endpoint-ов (failure rate >10% за 1 час) с уведомлением клиента
- **Retry timeline** - визуализация когда и почему были повторные попытки для конкретного события
Stripe Webhook Dashboard показывает каждую попытку доставки с response body, headers, timing. Svix (webhook-as-a-service, используется Clerk, Resend) хранит полный audit log 90 дней. При успешном 200 OK хранит тело ответа получателя - это помогает дебажить почему обработка прошла некорректно даже при успешной доставке.
Если webhook вернул 200 OK, значит событие обработано корректно и можно не хранить историю доставки.
200 OK означает только что HTTP-запрос был принят. Бизнес-логика на стороне получателя могла выполниться с ошибкой, данные могли попасть в исключение после ответа 200. Delivery log + audit trail обязательны для дебага реальных инцидентов.
В продакшне разрыв между 'webhook доставлен' и 'событие обработано корректно' - источник сложнейших багов. Shopify имеет кейсы где 200 OK возвращался, но запись в БД на стороне мерчанта фейлилась из-за дедлока. Без логов - невозможно доказать что проблема не на стороне Shopify.
Success rate webhook endpoint-а упал с 99% до 60% за последний час. Какой первый шаг в диагностике?
Итоги
- **At-least-once + идемпотентность = пара**: at-least-once гарантирует отсутствие потерь, идемпотентность по event_id гарантирует отсутствие дублей - вместе они дают практический exactly-once на уровне бизнес-эффекта.
- **DLQ - не мусорная корзина, а инструмент восстановления**: события в DLQ должны быть replay-ablе с rate limiting, клиент должен видеть их через dashboard и иметь возможность ручного retry.
- **200 OK не равно успешной обработке**: delivery log с response body, latency percentiles и endpoint health scoring превращают webhook-доставку из чёрного ящика в наблюдаемый процесс.
Связанные темы
Webhook delivery guarantees строятся на фундаментальных паттернах надёжных систем:
- Webhooks (основы) — Предыдущий урок - HTTP механика, HMAC-подписи, retry policies. Этот урок - операционная сторона надёжной доставки
- Message Queues — DLQ - прямой аналог Dead Letter Queue в Kafka/SQS/RabbitMQ. Те же паттерны: retry exhaustion, replay, monitoring
- Idempotency — At-least-once delivery без идемпотентности получателя бесполезна - дубли создают двойные списания и некорректное состояние данных
Вопросы для размышления
- Endpoint клиента намеренно возвращает 200 OK даже при ошибках обработки (чтобы не триггерить retry). Как провайдер webhooks может обнаружить что реальная обработка фейлится?
- Как спроектировать систему где webhook-события должны обрабатываться строго по порядку для каждого заказа, но в любом порядке между разными заказами?
- Клиент хочет получать webhook-события в режиме exactly-once. Объяснить почему это невозможно гарантировать на инфраструктурном уровне и что можно предложить вместо этого.