Telegram Bot API 429:用 retry_after 安排发送队列
Telegram Bot API 发送请求因限流返回 parameters.retry_after 时,应至少等待指定秒数,再重复请求。把截止时间持久化,并协调使用同一机器人的所有 worker;仅让一个进程休眠,无法减慢其他进程。网络超时则是另一种情况:没有响应时,发送可能已经发生,不能直接进入同一个自动重试分支。
关键结论
- 读取 Bot API 的结构化响应,不要从
description中提取数字。 - 官方发送限制是调度约束,不是吞吐量保证。
- 入站消息落库与出站发送冷却必须分开。
- 重启后仍要遵守截止时间,并重新确认延迟回复是否还有意义。
Telegram 发送限制实际约束什么
官方 Bots FAQ 建议单个聊天不要超过每秒一条消息;群组发送上限为每分钟 20 条;未启用付费广播时,批量通知约为每秒 30 条。短时突发也可能最终触发 429。这些限制作用于不同范围:把消息分散到多个聊天,并不会消除机器人整体的广播约束。
不要据此承诺低于这些数字就一定成功。具体方法的规则和实际响应仍然有效。本文处理普通发送队列,不讲付费广播配置,也不把所有 429 都解释成同一种原因。
Bot API reference 将 ResponseParameters.retry_after 定义为可选整数:触发流量控制后,再次请求前还需要等待的秒数。这是 JSON 字段,不是通用 HTTP 响应头契约。字段缺失时不能默认为零。
需要读取发送结果,就使用独立 API 请求。Webhook 响应体与独立 sendMessage 的比较 解释了为什么把方法嵌入 Telegram webhook 响应后,应用无法取得该方法的结果。
建立持久化发送通道,而不是休眠循环
以下是应用设计建议,不是 Telegram 的额外保证。
发送前,为每个预期回复保存一条本地任务,记录机器人身份、目标聊天、已批准内容、过期策略、尝试次数和最早执行时间。这些是应用自己的字段,不要加进 Bot API 请求。对业务意图去重,避免同一个入站更新重复投递时创建多条回复任务。
设置三层调度门槛:
- 机器人层:协调所有共享该机器人的进程总流量。
- 聊天层:分别控制每个目标的节奏,包含群组限制。
- 任务层:检查重试截止时间、过期、取消和执行租约。
原子领取任务,避免两个 worker 同时发送。真正调用网络前,再检查共享门槛。收到限流响应后,原子延长冷却时间并把任务交回调度器,不要保持 webhook 请求连接来等待。
retry_after 不说明究竟触发了哪一个限流范围。保守的初始策略是暂停该机器人的整个发送通道,等待返回的时长。这可能拖延其他聊天,但比假定只影响当前聊天更可靠。明确标注这是应用策略,再依据观测调整,不要猜测服务器内部算法。
将 retry_after 转成持久化截止时间
下面的 JavaScript 只演示结果分类,不发送请求,也不包含数据库实现,更不是已执行的生产测试。receivedAtMs 是客户端收到响应的时间,currentNotBeforeMs 是存储中已有的共享冷却截止时间。
function telegramFloodDecision(body, receivedAtMs, currentNotBeforeMs = 0) {
if (body?.ok !== false || body.error_code !== 429) {
return { action: 'classify_other_result' };
}
const seconds = body.parameters?.retry_after;
if (!Number.isSafeInteger(seconds) || seconds < 0) {
return { action: 'hold_for_review' };
}
const deadline = receivedAtMs + seconds * 1000;
if (!Number.isSafeInteger(deadline)) {
return { action: 'hold_for_review' };
}
return {
action: 'reschedule',
notBeforeMs: Math.max(currentNotBeforeMs, deadline)
};
}
返回值名称属于本地应用。用事务保存最大的截止时间;普通的先读后写可能覆盖另一 worker 刚保存的更长冷却。同步 worker 时钟,或使用共享调度时钟。需要随机抖动时只能增加等待,不能缩短 Telegram 要求的时间。
到期仅表示可以再次尝试,不表示一定成功。设置尝试次数与任务年龄预算;后续响应要求更长等待时,继续保存新的截止时间。任务过期或人工已经回复时,应取消或复核,而不是发出陈旧内容。这些预算与取消规则是业务策略,不是 Telegram 限制。
分开处理拒绝、未知结果与入站接收
| 观测结果 | 建议处理 |
|---|---|
有效限流响应,包含 retry_after | 保存冷却;所有门槛通过后才重试 |
429 没有可用等待值 | 暂停并分类,不忙循环,不当作零等待 |
sendMessage 成功 | 保存返回的 Message,不要重新安排同一意图 |
| 网络超时或响应丢失 | 标记结果未知,核对或人工复核后再决定 |
| 其他 API 错误 | 分别检查权限、目标和请求内容 |
本地任务 ID 能减少系统内部的重复调度,不是远端恰好执行一次的保证。发送成功后、保存响应前发生崩溃,仍会留下未知结果。不能把 outbox 本身描述成完全消除重复消息。
入站健康是另一层边界。验证并持久化收到的更新后,独立确认接收,不必等待出站任务结束。如果连更新都没有收到,使用 getWebhookInfo 诊断指南。pending_update_count 统计等待投递的更新,不是你的发送队列长度。
不要混用 UnifyPort 契约
UnifyPort 非官方接口连接消息账号,使用另一套文本发送契约。错误信封包含 error.code、error.numeric_code、error.message,并提供 request_id 用于追踪。错误参考 没有定义 Bot API 的 parameters.retry_after;不要原样复制上述分类器到 UnifyPort 客户端。
跨渠道 worker 可以共享任务存储,但必须为不同契约使用不同结果分类器。保留 HTTP 状态与结构化错误。请求追踪指南 也明确指出,X-Request-Id 不是已文档化的发送幂等键。非官方接口不会消除上游限制,也不会让未知结果可以安全地重复发送。
发送错误还要与接收器返回的 HTTP 状态分开。Webhook 投递参考 说明 UnifyPort 的投递重试立即进行,不遵循 Retry-After。让接收器返回 429,不是安排出站冷却的方法。
验收检查与常见问题
使用本地响应样本测试,不要刻意向 Telegram 制造洪峰。覆盖两个 worker 共用机器人、冷却期间重启、较长等待晚于较短等待到达、缺少 retry_after、等待期间取消任务和发送响应丢失。确认任务不会早于已保存截止时间执行,未知结果有独立分支。这些是建议检查,不是已完成测试。
retry_after 的单位是秒还是毫秒?
秒。调度器用毫秒存储时,需要显式换算。
增加 worker 能解决 429 吗?
仅增加数量不能。共用机器人的 worker 也共享其约束;没有协调的并发可能增加被拒绝的请求。
等待结束就保证发送成功吗?
不保证。它只满足此次响应的等待要求,其他门槛、权限和后续响应仍然有效。
超时也使用同样的重试规则吗?
不应如此。没有响应的超时不是已确认的限流拒绝,发送结果可能未知。
下一步与参考资料
先为现有机器人发送器加入持久化冷却,验证崩溃边界,再提高并发。消息账号客户端应从 UnifyPort 错误参考 开始,不要假定兼容 Bot API。
官方资料核对日期:2026-10-18。
让消息接入变成一条稳定的产品管线。
先用统一 API 跑通发送,再用标准事件把所有入站消息接回业务系统。