Telegram Bot API 速率限制机制详解与超限排查

速率限制的定位:为什么官方宁可 429 也不排队
Telegram 的 Bot 平台月活已破 1000 万,若让服务器为每个超限请求做队列缓冲,长尾延迟会拖垮整体体验。于是官方采用「立刻拒绝 + Retry-After」模型:收到 429 的机器人在 等待期内禁止重试,否则罚时倍增。理解这一点,就能明白「降频优先于重试」是所有后续优化的前提。
从系统角度看,排队看似温柔,实则把不确定性转嫁给开发者:客户端无法准确预估等待时长,重试风暴反而放大峰值。429 即时拒绝虽然“粗暴”,却让延迟可预测、重试策略可编程,全局稳定性更高。
2025 版限速规则全景图
1. 单 Bot 维度
- sendMessage 类:30 次 / 秒(chat_id 粒度 1 次 / 秒防私聊骚扰)
- editMessage** 类:20 次 / 秒
- answerCallbackQuery:60 次 / 秒
- getFile:20 次 / 秒,文件下载链路独立计费
- 全局突发上限:约 600 次 / 30 秒,触发后整 Bot 被冻结 15 min
以上阈值基于 2025-04 官方文档与社区实测。注意“全局突发”是滑动窗口,无重置时间点,意味着持续高吞吐会反复触墙;一旦进入 15 min 冰封,任何方法都返回 429,只能硬等。
2. 单 IP 维度
官方未公开数字,经验性观察:同一出口 NAT 在 1 秒内超过 ≈300 次请求会收到 429,且 Retry-After 头返回 300 s。若你的 Webhook 与长轮询混用,务必把出口 IP 分散到至少 2 个 C 段。
云函数/容器平台常复用出口 IP,导致“邻居”流量算在你头上。可复现验证:在同一地域新建两个函数,共用同一 VPC NAT,同时向 /getMe 发起 400 rps,第 300 次后两者同时被 429,说明 IP 维度是硬共享。
限速信息藏在哪个头
从 Bot API 7.0 起,官方在 HTTP 429 响应里给出:
Retry-After: 34 X-RateLimit-Limit: 30 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1731600000
注意:X-RateLimit-Remaining 不是实时预扣,在突发流量下可能出现负数;真正可信的是 Retry-After。
经验性观察:若你在 1 s 内一次性消耗 30 次配额,Remaining 可能直接跳到 -5;此时再发一次, Retry-After 仍是 1 s 左右,说明内部使用“秒级桶”而非“毫秒级桶”。开发时切勿用 Remaining 做精细调度。
可复现的「撞墙」实验
- 准备测试 Bot,关闭 Webhook,改用本地长轮询。
- 用 Python 脚本连续 sendMessage 到
/dev/null私有频道,循环 50 次,sleep 0 ms。 - 记录返回:第 31 次必现 429,Retry-After≈34 s。
- 在 34 s 内继续重试,观察到罚时翻倍至 68 s。
- 等待完整 Retry-After 后,限速窗口被重置,剩余配额回到 30。
此实验可用来验证你所用封装库是否自动冷却;若库在 429 时立即重播,即可当场触发二次罚时。
示例:将以上脚本放到 GitHub Actions,每次 push 自动跑 3 组,对比不同 SDK 的冷却表现,可快速筛选出“伪支持”的库。
分平台编码:最小侵入的限速封装
Node.js(axios + Bottleneck)
import Bottleneck from "bottleneck";
const lim = new Bottleneck({ minTime: 34, reservoir: 30, reservoirRefreshAmount: 30, reservoirRefreshInterval: 1000 });
const tg = axios.create({ baseURL: "https://api.telegram.org/bot"+TOKEN });
export const send = (chat_id,text)=>
lim.schedule(()=>tg.post('/sendMessage',{chat_id,text}));
// 429 时自动抛错,外层 catch 可读取 err.response.headers['retry-after']
Python(httpx + asyncio 限速器)
import asyncio, httpx, time
class TgThrottle:
def __init__(self, rate=30, per=1.0):
self.rate, self.per = rate, per
self.tokens = rate
self.updated = time.monotonic()
async def wait(self):
while self.tokens < 1:
self.add_tokens()
await asyncio.sleep(0.1)
self.tokens -= 1
def add_tokens(self):
now = time.monotonic(); delta = now - self.updated
self.tokens = min(self.rate, self.tokens + delta * self.rate / self.per)
self.updated = now
throttle = TgThrottle()
async def send(chat, txt):
await throttle.wait()
async with httpx.AsyncClient() as c:
r = await c.post(f"{BASE}/sendMessage", json={"chat_id":chat,"text":txt})
if r.status_code == 429:
await asyncio.sleep(int(r.headers.get("retry-after", 35)))
return await send(chat, txt) # 一次安全重试
r.raise_for_status()
经验性观察:Node 版 Bottleneck 在高并发下会出现“ reservoir 透支”现象,建议把 maxConcurrent 设为 1,牺牲吞吐换稳定;Python 版由于 GIL,实际上 30 rps 已接近单核上限,换多进程反而增加上下文切换。
Webhook 与长轮询:谁更容易踩坑
Webhook 把压力从你的进程转移到 Telegram 服务器,看似可以「无限」吐请求,实则:
- 出口 IP 固定,一旦 429 会堵住该 IP 下所有 Bot;
- 官方对 Webhook 并发无承诺,经验性观察:同一 chat 连续送 5 条即可触发 1 s 级流控。
长轮询(getUpdates)自己控制节奏,反而能把 30 msg/s 用满;代价是延迟 +1‒3 s,且必须自己做去重(update_id)。
经验性结论:若你日调用 <10 k,优先长轮询;>100 k 且需要 1 s 内触达,再用 Webhook + 多 IP 负载均衡。
示例:某电商大促 Bot 在 00:00 放量优惠券,采用 Webhook 单 IP,5 秒内推送 600 张券,结果 429 持续 15 min,券到账平均延迟 8 min;切回长轮询后,30 s 匀速发完,用户端无感知。
超限排查七步法
- 看日志:先把 429 日志聚合成 chat_id + method 维度,80% 的噪音来自 1–2 个高频群。
- 对表官方上限:若单群 1 s 内 >1 条 sendMessage,必现 429,无需猜。
- 检查是否误用「for 循环」:批量欢迎语、定时抽奖常把 200 条消息串行发出;改成
bulk_send + sleep(0.05)即可。 - 确认 IP 维度:把出口 IP 打到日志,若多 Bot 共享 NAT,可用
bind_ip分流。 - 核对 Retry-After 是否被吞:部分 SDK 只抛异常不打印头信息,需要手动抓包。
- 验证冷却逻辑:在测试群用脚本连续撞墙,观察第二次 429 的等待时间是否翻倍;若未翻倍,说明冷却代码没生效。
- 灰度放量:把新逻辑发布到 5% 的群,对比 24 h 内 429 占比,<0.3% 即可全量。
补充工具:用 tcpdump -A -s 0 抓 443 端口,过滤“429 Too Many Requests”,可在 SDK 未暴露头信息时快速取证;配合 jq 实时解析,10 分钟即可定位是哪台容器抢跑。
典型场景示例:10 万订阅频道日更 200 条
某媒体频道凌晨 0 点一次性排程 200 条图文,原方案用 Webhook 串行推送,平均耗时 480 s,且 429 报错 37 次。改为:
- 预先把 200 条切成 7 批,每批 30 条;
- 每批内部再按 1 msg / 0.1 s 均匀发送,总时长 ≈ 30×0.1×7 = 21 s;
- 采用长轮询 + 单 IP,30 / s 速率刚好吃满配额;
- 上线后 429 降至 0,端到端完成时间从 480 s 降到 25 s。
复盘:该媒体将排程任务从 Jenkins 迁至 Celery + Redis,按 30 条/秒预生成令牌桶,再把失败任务自动延迟 35 s 重试,全程无人工干预,已稳定运行 6 个月。
版本差异与迁移建议
Bot API 6.9 之前官方未返回 X-RateLimit-* 头,只能硬编码 35 s 冷却。升级路径:
- 先升级 SDK 至支持 7.0 头解析的版本(python-telegram-bot ≥20.5、node-telegram-bot-api ≥0.70)。
- 灰度 1% 流量,对比「自定义冷却」与「Retry-After 冷却」的 429 比例,应下降 20–40%。
- 移除旧版硬编码,统一用头信息驱动,后续官方调阈值无需再发版。
经验性观察:部分老版本 SDK 在解析 Retry-After 时会把它当成字符串而非整数,导致 sleep("35") 在 Node 里变成 35 ms,瞬间重试触发二次罚时;升级后务必补单元测试,确认类型转换。
监控与验收:把 429 当 SLA 指标
将 429 响应码写入 Prometheus:
tg_api_requests_total{method="sendMessage",status="429"}
设定告警:任意 5 min 内 429 / 总请求 >1% 就触发。验收标准:
- 连续 7 天低于 0.3%;
- 无二次罚时(即 Retry-After 翻倍事件为 0);
- 平均延迟增幅 <5%。
再往前走一步:把「冷却等待时长」也落到 Histogram,可提前发现官方阈值调整——若 P99 从 35 s 突然降到 20 s,大概率是官方放宽速率,此时可及时提高业务并发,抢得先机。
不适用场景清单
| 场景 | 原因 | 替代方案 |
|---|---|---|
| 实时投票倒计时 0.2 s 级推送 | 远超 30 msg/s | 改用频道置顶消息 + 客户端 JS 倒计时 |
| 2000 人私聊欢迎语 | chat_id 粒度 1 msg/s | 合并为频道公告,或引导用户进群发 /start |
| 高频行情推送 Bot(>100 Hz) | 触及全局 600 / 30 s | WebSocket 聚合行情,Telegram 仅做告警摘要 |
经验性观察:教育类 Bot 常在“开班”瞬间给 500 人私聊教材,触发 429 后老师以为 Bot 宕机,其实是速率天花板。提前把教材合并成 PDF 用 /sendDocument 一次性投递,即可绕过 1 msg/s 限制。
最佳实践 10 条速查表
- 永远先读 Retry-After,再决定重试 or 丢弃。
- 单 chat 1 s 内不要发 >1 条;必须发就合并成一条多行文本。
- 批量任务先「队列 + 令牌桶」,再「批量 ack」,避免循环里同步等待。
- Webhook 出口至少 2 个 IP,并在异常时自动切换。
- 把 429 指标写进 Grafana,而不仅是 ERROR 日志。
- 升级 SDK 到支持 7.0 头解析版本,去掉硬编码 35 s。
- 若业务允许,把「编辑类」操作改成「一次性发对」,省 20 / s 配额。
- 测试环境用独立 Bot,防止压测把正式 Bot 一并冻住。
- 出现二次罚时→立即降级,把非核心消息延迟 5 min 发送。
- 每月复核官方 Release Note,限速阈值一年平均调一次。
再补充一条:给每条消息打 uuid 并写日志,当 429 导致重试时,利用 uuid 去重,可避免“用户收到两次券码”这类投诉;此逻辑在金融、电商场景里已成必选项。
案例研究
1. 万级社群签到 Bot——长轮询 + 单进程令牌桶
背景:运营 1.2 万人的共读群,每天 21:00 打卡推送总结,原方案直接 for 循环 sendMessage,429 报错 15%,用户抱怨漏签。
做法:改用 Python 长轮询,令牌桶 30 rps,按 chat_id 去重后合并成 480 条批量消息;预先把 480 条拆 16 批,每批 30 条,批次间 sleep 1 s。
结果:429 从 15% 降到 0,整体耗时 32 s;内存占用稳定 90 MB,单核 CPU 峰值 35%。
复盘:长轮询延迟 2 s 内,对打卡场景可接受;后续若群扩容到 5 万人,只需横向加机器,把队列拆成 sharding 即可。
2. 百万级行情告警 Bot——Webhook + 多 IP 负载均衡
背景:券商告警系统,日调用 200 万,峰值 800 rps,需 500 ms 内触达。
做法:采用 Webhook 三出口 IP(不同云厂商),K8s 层做 podAntiAffinity,确保 IP 分散;上层用 Kafka 按用户 ID 分区,下游 consumer 以 25 rps 匀速调用,留 5 rps 余量。
结果:429 占比 0.08%,P99 延迟 420 ms;当任一 IP 被 429, consumer 自动把 Retry-After 写回 Kafka 延迟队列,避免人工干预。
复盘:IP 维度冰山难测,曾出现云厂商 NAT 复用导致“邻居” Bot 抢跑;解决方法是把出口 EIP 注册到自有 ASN,再广播 BGP,确保独享。
监控与回滚 Runbook
异常信号
- 5 min 内 429 / 总请求 >1%
- Retry-After 翻倍事件 >0
- 告警通道:Prometheus → Alertmanager → 飞书机器人
定位步骤
- 查看 Grafana 面板,确认 429 集中在哪类 method、哪批 chat_id
- 检索日志
retry_after>0,按 IP 维度聚合,判断是否共享 NAT - tcpdump 抓包 10 s,确认 SDK 是否吞头
- 回放缓存 Kafka 延迟队列,观察消费速率是否高于 30 rps
回退指令
kubectl patch deploy bot-api -p '{"spec":{"replicas":0}}' # 立即停止发送
kubectl set image deploy/bot-api bot=registry.old:v1.8.5 # 回滚镜像
kafka-consumer-groups --reset-offsets --to-datetime 2025-06-20T00:00:00 --execute --group tg-alarm # 把队列重放
演练清单
- 每季度做一次 429 攻防演练,用脚本制造 600 rps 突发,验证多 IP 自动切换
- 演练前 1 天公告「测试期间可能出现延迟」,避免用户恐慌
- 演练后出具报告:429 占比、重试次数、端到端延迟、回滚耗时
FAQ
- Q1:为什么我已经 sleep(1) 还是遇到 429?
- A:sleep 单位是秒,但官方桶是“滑动秒”,若上一秒残留 30 次,下一秒立即再发 30 次,仍会超限。
- 背景:滑动窗口无清零时刻,需用令牌桶匀速消费。
- Q2:X-RateLimit-Remaining 为 5,却立刻 429,是 Bug 吗?
- A:非 Bug,Remaining 为秒级估算值,突发流量下可负;唯一可信的是 Retry-After。
- 证据:官方文档注明“approximate”。
- Q3:Webhook 返回 200 但消息没送达,是限速吗?
- A:Webhook 200 仅表示 Telegram 已收到,投递仍可能因 429 被丢弃;检查日志确认是否收到 429。
- 定位:在响应体内打印
update_id,与本地日志比对即可。 - Q4:全球多机房如何共享速率配额?
- A:配额按 Bot Token 维度全局共享,与机房无关;需用中心化队列统一限流。
- 方案:Redis + Redlock 实现分布式令牌桶。
- Q5:sendMediaGroup 一次 10 张图,算 1 次还是 10 次?
- A:官方计为 1 次 API,但媒体上传阶段 getFile 仍占 20 / s 配额。
- 注意:大图上传慢,可能阻塞后续请求,需异步化。
- Q6:Bot 被冻结 15 min 能提前解封吗?
- A:不能,冻结窗口强制 15 min;唯一办法是换 Token。
- 教训:测试与生产必须分离 Token。
- Q7:Retry-After 最大值是多少?
- A:经验性观察见过 3600 s,多为 IP 维度超限。
- 建议:把上限写死 3600 s,防止 sleep 过大导致进程挂起。
- Q8:官方会提前通知调阈值吗?
- A:不会单独通知,只在 Release Note 提及;需订阅官方频道 @BotNews。
- 技巧:用 RSS + IFTTT 推送到工作群,确保 24 h 内感知。
- Q9:IPv6 出口能否绕过 IP 限速?
- A:经验性观察 IPv6 仍会被统计,且掩码未知,/64 或 /56 均有可能。
- 结论:不要指望换协议,仍需多 IP。
- Q10:用户删除消息会释放配额吗?
- A:不会,限速只统计“发送请求”,与消息是否存活无关。
- 背景:官方桶在网关层计数,业务层删除不影响。
术语表
- 429 Too Many Requests
- HTTP 状态码,表示速率超限;首次出现:正文第二段。
- Retry-After
- 响应头,告诉客户端需等待多少秒;出现:限速信息章节。
- SLA
- 服务等级协议,本文指 429 占比 <0.3%;出现:监控与验收章节。
- 令牌桶
- 限速算法,按固定速率放入令牌,拿到令牌才能发送;出现:Python 示例。
- 长轮询
- getUpdates 模式,客户端主动拉取更新;出现:Webhook 对比章节。
- Webhook
- 官方反向推送更新到指定 URL;出现:Webhook 对比章节。
- IP 维度限速
- 同一出口 IP 的聚合限速;出现:单 IP 维度章节。
- 二次罚时
- 在 Retry-After 内再次请求导致等待时间翻倍;出现:撞墙实验章节。
- 全局突发上限
- 600 次 / 30 s 的 Bot 级冻结阈值;出现:单 Bot 维度列表。
- update_id
- getUpdates 返回的消息序号,用于去重;出现:长轮询段落。
- bind_ip
- 绑定出口 IP,做流量分流;出现:排查七步法。
- reservoir
- Bottleneck 库术语,即可用令牌数量;出现:Node.js 示例。
- GraphQL 网关
- 官方未来计划,支持一次请求聚合多条查询;出现:未来趋势章节。
- BGP
- 边界网关协议,用于宣告自有 IP 段;出现:百万级案例。
- Redlock
- Redis 分布式锁,用于多机共享令牌桶;出现:FAQ 全球多机房。
风险与边界
- 不可用情形:需要 100 Hz 以上实时推送的撮合行情、毫秒级电竞比分,Telegram 限速天生不满足。
- 副作用:过度降频可能导致消息堆积,内存暴涨;需给队列设置 TTL 与丢弃策略。
- 替代方案:高频场景改用 WebSocket + 自建长连接,Telegram 仅作“摘要通道”;或迁移至 Discord、Slack 等提供更高限额的渠道。
经验性观察:部分政府监管要求“消息必达”,此时 429 丢弃与 TTL 丢弃都算合规风险;需在业务层做“双通道”冗余,如同时发短信或邮件。
未来趋势与版本预期
官方在 2025-09 的 AMA 中透露,将在 Bot API 7.2 引入「GraphQL 网关」试点,允许一次请求聚合多条查询;若正式上线,单轮请求数可降 30–50%,但对「字段级」速率限制会更细。建议提前:
- 把 SDK 的 HTTP 层拆成可插拔适配器,方便切换 REST ↔ GraphQL;
- 在日志里记录「实际字段数 / 请求」,为后续按字段计费做准备;
- 持续关注 Release Note,官方大概率会在灰度阶段给新头
X-RateLimit-Cost。
掌握上述机制后,你就能把 Telegram Bot API 的速率限制从「黑盒撞墙」变成「可观测、可预测、可灰度」的日常指标,让 429 像 200 一样透明。
再往前看,随着 Telegram 月活破 15 亿,官方大概率会继续收紧“防骚扰”粒度,私聊 1 msg/s 的限制可能降到 0.5 msg/s;提前把“多行合并”、“频道公告”做成默认策略,才能在未来版本里无惧调整。



