工程实践 · 2026 年 9 月 21 日
设计“至少一次”投递的邮件 Webhook
了解如何利用重试、幂等键和签名验证,为邮件事件构建健壮的 Webhook 消费端,确保不会漏掉任何投递事件。
可靠事件投递的挑战
要让邮件 Webhook 实现至少一次投递,您需要构建这样一个系统:发送方以指数退避重试失败的请求,接收方保证幂等。由于网络不可靠、服务器会崩溃,您不能假定一次 HTTP 200 OK 就能保证事件已被处理。可靠性来自两者的结合:发送方的持久化重试队列,加上接收方的去重层。
当您集成 SendHQ 这样的邮件 API 时,您的应用需要知道邮件何时已送达、被退回或被标记为垃圾邮件。这些事件是异步的。如果您的 Webhook 端点在流量高峰期间宕机五分钟,您可能会丢失成千上万条关键的投递信号。这会在您的分析数据中造成缺口,并使系统无法对退信作出反应(而这对维护发件人信誉至关重要)。
可靠 Webhook 的构成
一个健壮的 Webhook 架构由三大支柱组成:签名验证、幂等处理和重试策略。
1. 签名验证
永远不要仅凭 IP 地址或请求体中是否带有 API 密钥,就信任发往您 Webhook 端点的 POST 请求,攻击者可以伪造这些信息。请改用 HMAC(基于哈希的消息认证码)签名。
发送方使用共享密钥对负载进行签名,并将签名附加到一个请求头中(例如 X-SendHQ-Signature)。接收方使用同一密钥重新计算哈希,并与请求头中的值进行比较。
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Use timingSafeEqual to prevent timing attacks
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature));
}
2. 幂等与去重
至少一次投递意味着发送方会持续发送事件,直到收到成功响应为止。如果您的服务器处理了事件,却在返回 200 OK 之前崩溃,发送方会再次发送该事件。如果没有幂等处理,您的数据库可能会把一次投递记成两次。
每个事件都必须有唯一的 event_id。您应当采用幂等键模式来跟踪已处理的事件。
工作流程:
- 接收 Webhook 负载。
- 检查
event_id是否已存在于processed_events表中。 - 如果已存在,立即返回 200 OK 并忽略请求体。
- 如果不存在,在同一个事务中处理该事件并记录
event_id。
3. 重试策略
对发送方而言,重试策略是必不可少的。常见的模式是带随机抖动的指数退避,例如:在 1 分钟、5 分钟、30 分钟、2 小时和 12 小时后重试。
如果接收方返回 4xx 错误(429 除外),通常表示客户端错误(例如签名无效),重试也无济于事。5xx 错误或超时则表示暂时性失败,此时重试必不可少。
负载示例
下面是您可能从 SendHQ 收到的一个典型投递事件负载:
{
"event_id": "evt_12345abcde",
"event_type": "delivered",
"timestamp": "2026-09-15T10:00:00Z",
"message_id": "msg_98765xyz",
"recipient": "user@example.com",
"metadata": {
"order_id": "ord_5544"
}
}
处理失败与边界情况
“慢消费者”问题
如果您的 Webhook 处理程序同步执行繁重的数据库写入或调用其他外部 API,您的端点就会超时。这会触发发送方的重试逻辑,导致可能拖垮服务器的“重试风暴”。
解决方案:将接收与处理解耦。
- 接收 Webhook。
- 验证签名。
- 将原始负载推入消息队列(如 RabbitMQ、SQS 或 Redis)。
- 立即返回 200 OK。
- 由单独的 worker 进程消费队列并更新数据库。
智能体就绪问题
当 AI 智能体由 Webhook 触发时,出现无限循环的风险会增加。如果智能体收到一个“delivered”事件后又发送了一封邮件,而这封邮件又触发了另一个“delivered”事件,就形成了循环。
请把发送邮件视为一种外部副作用。在没有人工审批或严格的状态机检查来确认该操作确有必要的情况下,智能体绝不应基于 Webhook 自动发送邮件。
生态对比
选择服务商时,可靠性往往取决于它们如何处理这些事件,以及产生这些事件的邮件量如何收费。
对于大批量的事务性邮件,成本差异非常明显。根据 Amazon SES 价格,按量计费的发送成本为每 1,000 封邮件 0.10 USD。相比之下,Postmark 价格起价为每月 15 USD、含 10,000 封邮件,超出部分每 1,000 封 1.20 至 1.80 USD。发送 50,000 封邮件时,SES 按量计费约为 5 USD,而 Postmark 的套餐约为 66 USD。
其他选择包括 Resend,它提供每月 3,000 封邮件的免费额度(每天上限 100 封),Pro 套餐为每月 20 USD、含 50,000 封邮件。Mailgun 起价为每月 15 USD、含 10,000 封邮件。SendGrid 已将免费版改为 60 天试用,Essentials 起价为每月 19.95 USD。
无论选择哪家服务商,决定您数据完整性的都是您消费这些事件的可靠程度。
工程师实施清单
- 签名验证:是否使用共享密钥和恒定时间比较函数验证请求体?
- 异步处理:端点是否在执行繁重的业务逻辑前返回 200 OK?
- 幂等性:是否对
event_id设置了唯一约束以防止重复处理? - 超时管理:超时设置是否低于服务商的超时,以避免重试重叠?
- 监控:是否为 Webhook 端点 5xx 响应激增设置了告警?
- DNS 健康状况:您的收件服务器是否配置正确?使用 SendHQ 邮件 DNS 检查工具等工具,确保您的基础设施可访问且配置正确。
- 身份验证标准:您是否已实施DKIM、SPF 和 DMARC,以确保出站邮件被受理,并减少需处理的“退信”Webhook 数量?
权衡总结
方案 | 优点 | 缺点
同步处理 | 实现简单,即时一致 | 超时风险高,容易引发重试风暴
基于队列的处理 | 高度可扩展,能扛住流量高峰 | 基础设施更复杂,最终一致
仅记录日志 | 开销低 | 除非手动翻日志,否则无法恢复漏掉的事件
幂等表 | 保证数据完整性 | 每个事件多一次数据库写入
总结
邮件 Webhook 的可靠性不在于杜绝失败,而在于为失败而设计。假设网络一定会出故障、事件一定会被投递不止一次,您才能构建出真正有韧性的系统。无论您是在为一个小项目管理 SPF 记录,还是在扩展一个庞大的事务性邮件系统,签名验证和幂等这两种模式始终是黄金标准。
使用 SendHQ 构建您的邮件基础设施。