← 中文资源

简体中文版本

为出站 Webhook 添加有界指数退避重试

用有界指数退避、抖动、Retry-After 支持和可重放的失败表,让出站 Webhook 在客户端点短暂故障时仍能可靠送达。

阅读英文原文

概述

出站 Webhook 应采用至少一次投递语义,而不是发一次 POST 就结束。把 2xx 视为接收方已确认;408、425、429 和 5xx 属于可重试结果;其余 4xx 通常表示请求永远不会成功,应立即记录为永久失败。重试间隔按指数增长并设置上限,再加入抖动,避免大量失败请求在同一时刻冲击正在恢复的端点。Retry-After 作为延迟下限处理,但最终仍受最大延迟约束,防止异常响应头把任务停放数天。

步骤

先创建持久化失败表,保存 payload、尝试次数、最后状态和错误:

CREATE TABLE webhook_failures (
  delivery_id uuid PRIMARY KEY,
  endpoint_url text NOT NULL,
  event_type text NOT NULL,
  attempts integer NOT NULL,
  last_status integer,
  last_error text NOT NULL,
  payload jsonb NOT NULL,
  failed_at timestamptz NOT NULL DEFAULT now()
);

用 BullMQ 的 Queue 和 Worker 实现调度。第一次投递使用 deliveryId 作为 jobId,后续使用 ${deliveryId}-${attempt},自定义 jobId 不能包含冒号。延迟计算如下:

const exponential = Math.min(BASE_DELAY_MS * 2 ** (attempt - 1), MAX_DELAY_MS);
const jittered = Math.floor(exponential * (0.5 + Math.random() * 0.5));
const delay = Math.min(Math.max(jittered, retryAfterMs(header) ?? 0), MAX_DELAY_MS);

先对指数延迟加抖动,再用 Retry-After 取下限,这样抖动不会把等待时间压到接收方要求的冷却时间以下。

Worker 在每次尝试时重新计算 HMAC 签名,并带上 x-webhook-delivery 和 x-webhook-attempt 请求头。网络错误和超时走重试;不可重试的 4xx 直接写入 webhook_failures。达到最大尝试次数后同样落表,便于人工重放。

验证

启动一个本地接收器,前两次返回 503 并带 Retry-After: 1,第三次返回 204。以 WEBHOOK_RETRY_BASE_MS=500 启动 Worker,再入队一个固定 delivery ID。接收器应依次打印 attempt 1、2、3,最后以 204 结束;查询 webhook_failures 应返回 0 行。随后让接收器固定返回 400,再投递一次,确认只产生一条永久失败记录且 attempts = 1,不会继续循环。

注意事项

重试不能无限进行,端点长时间下线时应依赖失败表和人工重放。不要在 job payload 中存放长期密钥,发送时再按端点 ID 加载。接收方必须按 delivery ID 去重,因为网络和调度器都可能造成重复投递。除了失败数量,还要监控最老等待任务的年龄;延迟队列持续增长往往意味着一次尚未告警的故障。