telegram-bots
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTelegram bots — the official Bot API
Telegram bots — 官方 Bot API
Telegram holds the chat. Your database holds everything you will act on later.
Every serious bot defect is an update that was processed twice, or one that was
never delivered because nobody asked for it.
The Bot API is not where bots fail. They fail at the seam: the update redelivered
after a crash, the member event that never arrives because of a default nobody
read, the payment confirmed from the wrong signal, the broadcast that trips 429
at user 31 and silently stops.
Read against the Bot API on 2026-08-25, at version 10.3 (released
2026-08-24). Re-check before quoting a
version: this API ships roughly monthly. Re-checked 2026-10-19: 10.3 is still
the latest. For a machine to read, Telegram serves the docs as Markdown at the
same path plus : , one file per method
() and a types index ().
That is official and parseable, but it is Markdown, not a JSON schema.
core.telegram.org/bots/api-changelog.mdcore.telegram.org/bots/api.md/bots/api/methods/getupdates.md/bots/api/types.mdDeep material, loaded on demand:
| Read | When |
|---|---|
| wiring polling or a webhook — the delivery contract, ordering, dedup, and the migration between them |
| taking money — Stars, invoices, the pre-checkout window, refunds, subscriptions |
| sending at volume, or moving files — every published limit and what to do at each |
| choosing or auditing a library — aiogram, grammY, Telegraf, python-telegram-bot, and what each hides |
| the bot has real traffic, or it stops answering everyone when Telegram is slow — a queue per chat, a ceiling, payments outside it, shutdown, alarms |
Runnable, and shipped beside this file:
— nine invariants a
correct handler holds, each with the mutant that makes it fail. watches a redelivered update processed
twice, an update lost to a crash at the offset seam, an acked update lost to a
dead worker, a redelivery answered "duplicate" about work that never happened, a
reply doubled by a crash before the done mark, a redelivered outbox row sent
twice, one charge granted twice across two updates, a reply dropped on 429, and
one payment granted twice. Standard library only.
— nine rules that keep one
slow handler from holding every chat (the poll does not wait for handlers,
per-chat order, a ceiling, a payment lane, flood measured from arrival, no
supersession at queue time, waiting callbacks answered, dedup, a per-chat cap),
each removed in turn by and watched failing.
fixtures/update_delivery.pypython3 fixtures/update_delivery.py --self-testfixtures/update_pump.py--self-testTelegram 承载聊天。你的数据库保存着之后要据此采取行动的一切。
每一个严重的 bot 缺陷,要么是同一条 update 被处理了两次,要么是它从未被投递,因为根本没人去索取。
Bot API 并不是 bot 失败的地方。它们失败在衔接处:崩溃后被重新投递的 update;因默认设置无人留意而从未到达的 member event;由错误信号确认的 payment;广播到第 31 个用户时触发 429 并悄然停止。
核对基准:Bot API 2026-08-25,版本 10.3(发布于 2026-08-24)。引用版本前请重新检查 :此 API 大约每月发布一次。复查日期为 2026-10-19:10.3 仍是最新版本。对于需要程序读取的机器,Telegram 会在相同路径加 后以 Markdown 提供文档:;每个方法各有一个文件(),另有一个类型索引()。这些是官方且可解析的文档,但它们是 Markdown,而不是 JSON Schema。
core.telegram.org/bots/api-changelog.mdcore.telegram.org/bots/api.md/bots/api/methods/getupdates.md/bots/api/types.md深入资料,按需加载:
| 阅读 | 何时 |
|---|---|
| 配置 polling 或 webhook——投递约定、顺序、去重,以及两者之间的迁移 |
| 收款——Stars、invoice、预结账窗口、退款、订阅 |
| 大批量发送或传输文件——所有已发布的限制及相应处理方式 |
| 选择或审计库——aiogram、grammY、Telegraf、python-telegram-bot,以及每个库隐藏了哪些细节 |
| bot 已有真实流量,或 Telegram 变慢时 bot 不再响应所有用户——每个聊天一个队列、容量上限、队列外支付、停机、告警 |
可运行,并随此文件提供:
——正确处理器应满足的九条不变量,每条都配有一个使其失败的变异用例。 会监测以下情况:被重新投递的 update 被处理两次;在 offset 边界处因崩溃而丢失的 update;已确认但因 worker 死亡而丢失的 update;重新投递时针对从未执行过的工作回复“duplicate”;在标记完成前崩溃导致回复被重复发送;重新投递的 outbox 行被发送两次;一次 charge 因两条 update 被授予两次;因 429 而丢弃的回复;以及一次 payment 被授予两次。仅使用标准库。
——九条规则,防止一个缓慢的处理器阻塞所有聊天(poll 不等待处理器、按聊天排序、容量上限、支付通道、基于到达时间测量 flood、入队时不发生覆盖、等待中的 callback 得到响应、去重、按聊天设置上限)。 会逐一移除这些规则并观察失败。
fixtures/update_delivery.pypython3 fixtures/update_delivery.py --self-testfixtures/update_pump.py--self-testWhich API you are on, decided once
你使用的是哪种 API,只需决定一次
| You need | Use | Why |
|---|---|---|
| A bot users add, in chats it is in | Bot API — this skill | HTTP, a token, no phone number, no ban risk |
| To read history a bot cannot see, or act as a person | MTProto — | a user account, with everything that costs |
| A web interface inside Telegram | Mini App — | a page plus |
A bot cannot read messages in a group without privacy mode off or a mention,
see a chat it was never added to, read history from before it joined, act on
behalf of a user, or download a file over 20 MB. Wanting any of those is the
signal to read — and to read its refusal first, because a
user account is a liability a bot token is not.
telegram-userbots| 你的需求 | 使用 | 原因 |
|---|---|---|
| 用户添加后、且它所在聊天中的 bot | Bot API——本 skill | HTTP、使用 token、无需电话号码、无封号风险 |
| 读取 bot 无法看到的历史记录,或以个人身份操作 | MTProto—— | 使用用户账户,以及伴随而来的所有成本 |
| Telegram 内的 Web 界面 | Mini App—— | 一个页面,以及必须验证的 |
bot 无法在未关闭 privacy mode 且未提及时读取群组消息,无法查看从未加入的聊天,无法读取加入前的历史记录,无法代表用户操作,也无法下载超过 20 MB 的文件。如果需要上述能力,就应阅读 ——并先阅读它的拒绝说明,因为用户账户带来的风险是 bot token 所没有的。
telegram-userbotsThe update is the event, and it arrives at least once
update 才是事件,而且至少会到达一次
python
undefinedpython
undefinedaiogram 3.x — the shape, not the framework
aiogram 3.x — the shape, not the framework
async def webhook(update: Update, db) -> web.Response:
await db.inbox_put(update.update_id, raw_body) # INSERT on a primary key,
return web.Response(status=200) # durable BEFORE the ack
async def webhook(update: Update, db) -> web.Response:
await db.inbox_put(update.update_id, raw_body) # INSERT on a primary key,
return web.Response(status=200) # durable BEFORE the ack
off the request path: a worker takes a pending row, does the work, marks it
off the request path: a worker takes a pending row, does the work, marks it
done — and a crashed attempt leaves the row pending for the next sweep
done — and a crashed attempt leaves the row pending for the next sweep
- **`update_id` is the only idempotency key you get.** It is stable across
redeliveries and it identifies an update — but it is IDENTITY, not a forever
guarantee of monotonicity: after ~a week idle the counter can restart from a
new random base, so never compare update_ids across a long gap to decide
"newer". Nothing else in an update identifies it: two identical messages a
second apart are two events, and the same event delivered twice is one.
- **Inbox before ack — a claim is a receipt, not completion.** The `INSERT` on a
primary key (never a `SELECT` then an `INSERT`: under a webhook up to
`max_connections`, default 40, deliveries of one update can be in flight at
once) writes a durable row the 200 is only then allowed to answer for. Work
runs off the request under a worker lease: a worker that dies leaves the row
pending and the sweep retries it. Claim-then-work inside the request is the
quiet data-loss shape — the crash leaves the claim standing, Telegram's
redelivery reads "duplicate", and the update is gone while every fixture
stays green.
- **Updates are kept for 24 hours** and no longer. A bot that is down for a day
has lost them, and nothing will say so — reconcile from your own state, never
from the assumption that the queue drained.
- Measured across eight live Telegram bots on this machine on 2026-08-25:
**zero of eight deduplicate on `update_id`.** Long polling hides it until the
first crash between "work done" and "offset confirmed".
- **`update_id` 是你唯一能获得的幂等键。**它在重新投递之间保持稳定,并用于标识一条 update——但它表达的是身份,而不是永久保证单调递增:空闲约一周后,计数器可能会从一个新随机基数重新开始,因此绝不要跨较长间隔比较 update_id 来判断谁“更新”。update 中没有其他内容可以标识它:相隔一秒的两条相同消息是两个事件,而同一个事件被投递两次则算作一个。
- **先写入 Inbox,再确认——claim 是收据,不是完成。**在主键上执行 `INSERT`(绝不要先 `SELECT` 再 `INSERT`:在 webhook 场景下,最多可同时有 `max_connections` 条投递在途,默认值为 40)会写入一条持久化记录,写入之后才允许返回 200。工作应在请求路径之外、以 worker lease 的方式运行:worker 崩溃后,记录会保持 pending 状态,并由下一轮扫描重试。在请求内部先 claim 再处理是一种不易察觉的数据丢失形态——崩溃后 claim 仍然存在,Telegram 的重新投递会被判定为“duplicate”,而 update 已经丢失,同时所有 fixture 仍然显示正常。
- **Update 只保留 24 小时。**宕机一天的 bot 会丢失它们,而且不会收到任何提示——应依据自身状态进行核对,绝不能假设队列已经清空。
- 于 2026-08-25 在本机上对八个在线 Telegram bot 进行测量:**八个 bot 中没有任何一个根据 `update_id` 去重。**Long polling 会掩盖这一问题,直到“工作完成”与“offset 已确认”之间首次发生崩溃。allowed_updates
drops three types by default
allowed_updatesallowed_updates
默认会丢弃三种类型
allowed_updatespython
await bot.set_webhook(
url=f"{origin}/tg/{SECRET_PATH}",
secret_token=WEBHOOK_SECRET, # 1-256 chars
allowed_updates=["message", "callback_query", "chat_member",
"pre_checkout_query", "my_chat_member"],
drop_pending_updates=False,
)The default — an empty list, and the value you get by not passing the parameter —
means "all update types except , and
". So a bot that tracks joins and leaves receives
nothing, the request returns , and the logs are clean. Name every
type you handle, explicitly, and re-run when you add one:
is set at subscription time, not at handler time.
chat_membermessage_reactionmessage_reaction_countok: truesetWebhookallowed_updatespython
await bot.set_webhook(
url=f"{origin}/tg/{SECRET_PATH}",
secret_token=WEBHOOK_SECRET, # 1-256 chars
allowed_updates=["message", "callback_query", "chat_member",
"pre_checkout_query", "my_chat_member"],
drop_pending_updates=False,
)默认值——空列表,也就是不传入该参数时得到的值——表示“除 、 和 外的所有 update 类型”。因此,一个追踪成员加入和退出的 bot 什么都收不到,请求仍返回 ,日志也一片干净。必须明确列出你要处理的每一种类型,并在新增类型后重新运行 : 是在订阅时设置的,而不是在处理时设置的。
chat_membermessage_reactionmessage_reaction_countok: truesetWebhookallowed_updatesWebhook or polling — and never both
Webhook 或 polling——但绝不能同时使用
getUpdatesgetUpdates- The webhook is the callback, so it needs the same defences as a payment
webhook: verify against the value you set with
X-Telegram-Bot-Api-Secret-Token, in constant time, and answer 401 with no detail when it fails. The URL is not a secret; the header is.secret_token - Answer fast, work later. Telegram retries on non-2xx and on a slow reply,
and a retry is a second delivery of the same . Acknowledge, then process — the claim above is what makes that safe.
update_id - is a decision, not a cleanup. It discards everything queued, including payments already made.
drop_pending_updates=True - Long polling is correct for development and for low-volume bots; a webhook is
correct when you have a public HTTPS endpoint and care about latency. Both are
in .
references/updates-and-delivery.md
设置 webhook 后, 将无法工作。这正是迁移时的隐患:针对生产 token 在本地运行 ,会依据最后执行的是哪一侧而悄悄接管生产环境或直接失败。一个 token,只能有一个消费者。
getUpdatesgetUpdates- **Webhook 是回调,因此需要与支付 webhook 相同的防护措施:**使用恒定时间算法,将 与通过
X-Telegram-Bot-Api-Secret-Token设置的值进行比较;验证失败时返回 401,且不泄露任何细节。URL 不是秘密,header 才是。secret_token - **快速响应,稍后处理。**Telegram 会在收到非 2xx 响应或响应过慢时重试,而重试意味着同一个 被再次投递。先确认,再处理——上述 claim 正是保证这一流程安全的关键。
update_id - **是一个决定,而不是清理操作。**它会丢弃队列中的所有内容,包括已经完成的支付。
drop_pending_updates=True - Long polling 适用于开发和低流量 bot;当你拥有公开的 HTTPS 端点并关注延迟时,webhook 是正确选择。两者都收录在 中。
references/updates-and-delivery.md
Limits are a design constraint, not an error path
限制是设计约束,而不是错误处理路径
| Limit | Value | What it means for the design |
|---|---|---|
| One chat | ~1 message/second | bursts are tolerated, then refused |
| One group | 20 messages/minute | a chatty group bot needs a queue |
| Bulk, all users | ~30 messages/second | a broadcast to 100k users is hours, not a loop |
| Paid broadcasts | up to 1000/second | 0.1 Stars per message past the free 30/s |
On 429 the response carries in seconds. Sleep exactly
that long and retry the same call — a fixed backoff either wastes the window or
trips the next one. A broadcast is a job with a rate limiter and a resume point,
not a loop; if it cannot resume, a crash at user 60 000 restarts at zero
and every earlier user is messaged twice.
parameters.retry_afterfor| 限制 | 数值 | 对设计的意义 |
|---|---|---|
| 单个聊天 | 约 1 条消息/秒 | 可以容忍突发流量,随后便会拒绝 |
| 单个群组 | 20 条消息/分钟 | 群聊中话多的 bot 需要队列 |
| 面向所有用户的批量发送 | 约 30 条消息/秒 | 向 10 万用户广播需要数小时,而不是一个循环 |
| 付费广播 | 高达 1000 条/秒 | 超出免费 30 条/秒的部分,每条消息收取 0.1 Stars |
收到 429 时,响应中的 会给出秒数。精确等待该时长,然后重试同一次调用——固定退避要么浪费等待窗口,要么触发下一轮限流。广播是一个带限流器和恢复点的任务,而不是一个 循环;如果任务无法恢复,在第 60 000 个用户处崩溃就会从零开始,之前每个用户都会收到两次消息。
parameters.retry_afterforMoney: the successful payment is the payment
资金:成功支付才代表支付完成
Digital goods and services are sold exclusively in Telegram Stars, currency
code , and is left empty for them.
XTRprovider_tokensendInvoice / createInvoiceLink → pre_checkout_query → successful_payment
answer within 10s deliver here- has a ten-second window. Miss it and the transaction is cancelled — so that handler validates against your own state and nothing slow. Anything that can take a second belongs after
answerPreCheckoutQuery.successful_payment - Deliver on , never on the pre-checkout. Pre-checkout is a question; the payment has not happened yet.
successful_payment - exists and a refund is not a deletion. Claw back what you granted, keyed on the charge id, exactly the way a card refund is handled — the arithmetic and the ordering are the same problem
refundStarPaymentcovers.stripe-billing
Full flow, subscriptions and the transaction ledger:
.
references/payments-stars.md数字商品和服务只能通过 Telegram Stars销售,货币代码为 ,此时 应留空。
XTRprovider_tokensendInvoice / createInvoiceLink → pre_checkout_query → successful_payment
answer within 10s deliver here- **的响应窗口只有十秒。**错过它,交易就会被取消——因此该处理器只能根据自身状态进行验证,不能执行任何慢操作。任何可能耗时一秒的工作都应放在
answerPreCheckoutQuery之后。successful_payment - **在 时交付商品,绝不要在预结账阶段交付。**预结账只是一个问题,此时支付尚未发生。
successful_payment - **确实存在,而且退款不是删除。**应按照处理银行卡退款的完全相同方式,依据 charge id 收回已经授予的内容——其中的计算和顺序问题与
refundStarPayment涵盖的内容相同。stripe-billing
完整流程、订阅和交易账本:
。
references/payments-stars.mdFiles have a ceiling, and it is lower than you think
文件存在上限,而且比想象中更低
- Download via : 20 MB. Above that the Bot API refuses, and no framework works around it.
getFile - Upload: 50 MB through the cloud Bot API.
- A local Bot API server removes both ceilings and changes the file paths
your code receives. That is an infrastructure decision, not a flag —
.
references/limits-and-files.md - is not a URL and not stable across bots. It is valid for your bot only; store it to resend cheaply, never as an archival reference.
file_id
- **通过 下载:20 MB。**超过该限制,Bot API 会拒绝处理,任何框架都无法绕过。
getFile - 通过云端 Bot API 上传:50 MB。
- 本地 Bot API server 会取消这两个上限,同时改变代码接收到的文件路径。这是一项基础设施决策,而不是一个开关——详见 。
references/limits-and-files.md - **既不是 URL,也不会在不同 bot 之间保持稳定。**它仅对所属 bot 有效;应将其作为缓存保存以便低成本重新发送,但绝不能作为长期归档引用。
file_id
What belongs in your database
哪些数据应由你的数据库保存
| Fact | Where it lives | Why |
|---|---|---|
| yours, unique | the only idempotency key |
| yours | Telegram will not list your users for you |
| the message you sent | yours, with | editing later needs both ids |
| entitlement bought with Stars | yours, keyed on the charge id | a refund must find it |
| yours, as a cache | cheap resend, not an archive |
Telegram is not a database and offers no way to enumerate the people who have
started your bot. If you did not write it down when it happened, it is gone.
A second channel, when the audience can lose Telegram. A bot is a channel
you do not own. If your users live where access to Telegram is restricted, or
could become so, plan for the day it stops reaching them: key users on your own
id rather than on alone, ask for a second contact (email, a web
account) at a moment it is useful to the user, and keep the product reachable
outside Telegram. A block then costs a channel, not the user list.
chat_id| 事实 | 存放位置 | 原因 |
|---|---|---|
已看到的 | 你的数据库,唯一 | 唯一的幂等键 |
| 你的数据库 | Telegram 不会为你列出用户 |
| 你发送的消息 | 你的数据库,并记录 | 后续编辑需要两个 ID |
| 使用 Stars 购买的使用权益 | 你的数据库,以 charge id 为键 | 退款时必须能找到它 |
| 你的数据库,作为缓存 | 用于低成本重新发送,而非归档 |
Telegram 不是数据库,也不会提供任何方法来枚举已开始使用你的 bot 的用户。如果事情发生时你没有记录下来,它就已经消失了。
**当受众可能失去 Telegram 时,准备第二个渠道。**bot 是一个你无法掌控的渠道。如果用户所在地区限制访问 Telegram,或未来可能限制访问,请提前规划 bot 无法触达他们的那一天:应基于你自己的 id 而不只是 来标识用户;在对用户有帮助的某个时刻请求第二联系方式(邮箱、Web 账户);并确保产品在 Telegram 之外仍然可用。这样,封锁带来的损失只是一个渠道,而不是整个用户列表。
chat_idBefore you ship
发布之前
- Every handler is idempotent on (§ The update is the event).
update_id - The webhook verifies the secret header and answers 401 without detail (§ Webhook or polling).
- names every type you handle — the default is not "all" (§
allowed_updatesdrops three types).allowed_updates - 429 sleeps for , and a broadcast can resume (§ Limits).
retry_after - Goods are delivered on , not on pre-checkout (§ Money).
successful_payment - The poll does not wait for handlers: a queue per chat, a ceiling across
chats, payments outside it ().
references/high-load.md
- 每个处理器都基于 实现幂等(参见“update 才是事件”)。
update_id - Webhook 验证 secret header,失败时返回 401 且不泄露细节(参见“Webhook 或 polling”)。
- 明确列出你处理的每一种类型——默认值不是“全部”(参见“
allowed_updates默认会丢弃三种类型”)。allowed_updates - 遇到 429 时按照 等待,并且广播任务可以恢复(参见“限制”)。
retry_after - 商品在 时交付,而不是在预结账阶段交付(参见“资金”)。
successful_payment - Poll 不等待处理器:每个聊天一个队列,所有聊天共用容量上限,支付在队列之外处理()。
references/high-load.md
Triggers
触发词
More phrases that route here: "grammy", "telegraf", "python-telegram-bot", "getUpdates",
"pre_checkout_query", "high load bot", "бот тормозит", "бот апи", "Telegram-бот", "Telegram-бота", "телеграм-бота".
更多会路由到这里的短语:"grammy", "telegraf", "python-telegram-bot", "getUpdates",
"pre_checkout_query", "high load bot", "бот тормозит", "бот апи", "Telegram-бот", "Telegram-бота", "телеграм-бота"。