返回博客列表
接口限速

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

Telegram 官方团队
Telegram Bot API 速率限制, Telegram 429 报错排查, Telegram Bot 超限解决方法, 如何降低 Telegram Bot 请求频率, Telegram Bot 重试策略配置, Telegram Bot 高并发限速方案, Telegram API 调用次数限制, Telegram Bot 日志分析方法

速率限制的定位:为什么官方宁可 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 做精细调度。

可复现的「撞墙」实验

  1. 准备测试 Bot,关闭 Webhook,改用本地长轮询。
  2. 用 Python 脚本连续 sendMessage 到 /dev/null 私有频道,循环 50 次,sleep 0 ms。
  3. 记录返回:第 31 次必现 429,Retry-After≈34 s。
  4. 在 34 s 内继续重试,观察到罚时翻倍至 68 s。
  5. 等待完整 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()
提示:以上代码默认「单次安全重试」。若业务允许丢消息,可把 429 直接抛给上游,不进入重试分支,降低雪崩风险。

经验性观察: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 匀速发完,用户端无感知。

超限排查七步法

  1. 看日志:先把 429 日志聚合成 chat_id + method 维度,80% 的噪音来自 1–2 个高频群。
  2. 对表官方上限:若单群 1 s 内 >1 条 sendMessage,必现 429,无需猜。
  3. 检查是否误用「for 循环」:批量欢迎语、定时抽奖常把 200 条消息串行发出;改成 bulk_send + sleep(0.05) 即可。
  4. 确认 IP 维度:把出口 IP 打到日志,若多 Bot 共享 NAT,可用 bind_ip 分流。
  5. 核对 Retry-After 是否被吞:部分 SDK 只抛异常不打印头信息,需要手动抓包。
  6. 验证冷却逻辑:在测试群用脚本连续撞墙,观察第二次 429 的等待时间是否翻倍;若未翻倍,说明冷却代码没生效。
  7. 灰度放量:把新逻辑发布到 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。
注意:频道消息不受「1 msg / 1 s / chat」限制,但 editMessage 仍限速 20 / s;若你事后批量改错字,依旧会被 429。

复盘:该媒体将排程任务从 Jenkins 迁至 Celery + Redis,按 30 条/秒预生成令牌桶,再把失败任务自动延迟 35 s 重试,全程无人工干预,已稳定运行 6 个月。

版本差异与迁移建议

Bot API 6.9 之前官方未返回 X-RateLimit-* 头,只能硬编码 35 s 冷却。升级路径:

  1. 先升级 SDK 至支持 7.0 头解析的版本(python-telegram-bot ≥20.5、node-telegram-bot-api ≥0.70)。
  2. 灰度 1% 流量,对比「自定义冷却」与「Retry-After 冷却」的 429 比例,应下降 20–40%。
  3. 移除旧版硬编码,统一用头信息驱动,后续官方调阈值无需再发版。

经验性观察:部分老版本 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 sWebSocket 聚合行情,Telegram 仅做告警摘要

经验性观察:教育类 Bot 常在“开班”瞬间给 500 人私聊教材,触发 429 后老师以为 Bot 宕机,其实是速率天花板。提前把教材合并成 PDF 用 /sendDocument 一次性投递,即可绕过 1 msg/s 限制。

最佳实践 10 条速查表

  1. 永远先读 Retry-After,再决定重试 or 丢弃。
  2. 单 chat 1 s 内不要发 >1 条;必须发就合并成一条多行文本。
  3. 批量任务先「队列 + 令牌桶」,再「批量 ack」,避免循环里同步等待。
  4. Webhook 出口至少 2 个 IP,并在异常时自动切换。
  5. 把 429 指标写进 Grafana,而不仅是 ERROR 日志。
  6. 升级 SDK 到支持 7.0 头解析版本,去掉硬编码 35 s。
  7. 若业务允许,把「编辑类」操作改成「一次性发对」,省 20 / s 配额。
  8. 测试环境用独立 Bot,防止压测把正式 Bot 一并冻住。
  9. 出现二次罚时→立即降级,把非核心消息延迟 5 min 发送。
  10. 每月复核官方 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 → 飞书机器人

定位步骤

  1. 查看 Grafana 面板,确认 429 集中在哪类 method、哪批 chat_id
  2. 检索日志 retry_after>0,按 IP 维度聚合,判断是否共享 NAT
  3. tcpdump 抓包 10 s,确认 SDK 是否吞头
  4. 回放缓存 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;提前把“多行合并”、“频道公告”做成默认策略,才能在未来版本里无惧调整。

BotAPI限速429排查重试