Broadcasting to Your Telegram Bot's Users Without Getting Rate-Limited
Sooner or later your Telegram bot needs to tell everyone something — a new feature, an outage, a price change. The naive version is a for loop over your user table, and it fails in two ways at once: Telegram rate-limits you, and your container gets recreated somewhere in the middle. The published limits are specific: "In a single chat, avoid sending more than one message per second", "In a group, bots are not be able to send more than 20 messages per minute", and "For bulk notifications, bots are not able to broadcast more than about 30 messages per second" (Telegram Bot FAQ). Telegram's own advice is to spread notifications over 8-12 hours. At that pace a broadcast is a long-running job that has to survive a restart, and designing it that way is most of the work.
The limits you're working against
| Limit | What it applies to |
|---|---|
| ~1 message per second | Any single chat |
| 20 messages per minute | A group chat |
| ~30 messages per second | Bulk notifications across chats |
Exceed them and you get 429 responses. The FAQ's suggested remedy for a large audience is spreading the send over longer intervals — it names 8-12 hours — rather than trying to push through.
Do the arithmetic before you design anything. At a conservative 25 messages per second, 10,000 users take about seven minutes. At 100,000 they take over an hour. Either way it's longer than a request, longer than a webhook handler, and long enough that a deploy can land in the middle.
Why a broadcast can't live in memory
Here's the failure that's specific to running in a container, and it's silent.
The container is recreated on every deploy and every configuration change. If your broadcast is a loop over an in-memory list, a deploy at minute four of a seven-minute send leaves you with no idea who received it. Your options at that point are both bad: send again and annoy the people who already got it, or don't and leave the rest uninformed.
There's no graceful way out either — there's about one second between the stop signal and the kill, which is not enough to finish anything.
So the state has to be on disk, updated as you go. A table on a volume is enough:
CREATE TABLE broadcast_targets (
broadcast_id INTEGER NOT NULL,
chat_id INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT 'pending', -- pending | sent | blocked | failed
PRIMARY KEY (broadcast_id, chat_id)
);
Mark each row the moment its send returns. On startup, look for a broadcast with pending rows and carry on. A restart becomes a pause rather than a loss, which is the same principle as designing for restarts generally — the platform will recreate your container whether or not you were ready.
Pace yourself, and handle the two errors that matter
Run the loop as background work inside the same app — you don't need a second application, and you couldn't share a volume with one anyway.
Pace deliberately. Sleep between sends to sit under the bulk ceiling rather than discovering it. Something like 40ms between messages puts you around 25 per second with headroom. If you have a day to do it in, go far slower; nothing is gained by finishing early.
Back off when told to. A 429 carries the wait Telegram wants from you — python-telegram-bot surfaces it as RetryAfter, whose retry_after attribute is "Time in seconds, after which the bot can retry the request" (python-telegram-bot docs). Sleep for exactly that and retry the same user. Do not retry immediately, and do not skip them.
Treat "forbidden" as permanent. When someone blocks your bot, the send fails with a rights error — the same library raises Forbidden, "Raised when the bot has not enough rights to perform the requested action". That user is never coming back on this broadcast, so mark them blocked and move on. Retrying them is how you burn your rate budget on people who can't receive anything.
That last one compounds. A bot that never records blocks accumulates dead chat IDs forever, and eventually most of your "audience" is people who left. Marking them at send time keeps the list honest for free.
Send fewer messages instead
The most effective rate-limit strategy is having less to send.
- Segment. Most announcements are relevant to a subset — people who used the feature, people in a language, people active in the last 90 days. A broadcast to 2,000 relevant users beats one to 20,000 indifferent ones on every axis, including how many of them block you afterwards.
- Let people opt out. A
/notifications offcommand costs you an afternoon and saves you from being blocked, which is the permanent version of opting out. - Batch your news. One monthly note lands better than four weekly ones, and it's a quarter of the sends.
Bots get blocked for being noisy far more often than for being useless, and every block is a user you can't reach again even when it matters.
What about paid broadcasts?
Telegram offers a paid tier for this. The FAQ describes enabling paid broadcasts to reach "up to 1000 messages per second", at "0.1 Stars per message" for messages beyond the free 30 per second.
It's worth knowing about and it's almost certainly not for you. The free ceiling of 30 a second clears 100,000 users in under an hour, and the only thing paying buys is finishing faster. Unless your announcement is genuinely time-critical — a security issue, an outage — spreading it out costs nothing and is what Telegram asks for anyway.
The Discord equivalent, briefly
Discord has no broadcast mechanism at all: no way to message every user of your bot. You can post once per server, which is a much smaller loop, and even that runs into per-route rate limits and the invalid-request ceiling.
The pattern is the same — pace it, persist which servers you've done, and treat missing-permission errors as permanent rather than retrying them. The scale is just far kinder.
FAQ
How fast can a Telegram bot send messages?
About 30 messages per second for bulk notifications, roughly one per second in any single chat, and 20 per minute in a group.
What happens if I go over?
You get 429 responses. Honour the wait time the error carries and retry that user rather than skipping them.
How long should a broadcast take?
Telegram suggests spreading bulk notifications over 8-12 hours. Unless it's urgent, slower is strictly better.
Why does my broadcast restart from the beginning?
Your progress is probably in memory, and the container is recreated on every deploy and configuration change. Record each send in a table on a volume.
What do I do about users who blocked the bot?
Mark them permanently and stop sending. Retrying them consumes rate budget you need for real users.
Do I need a separate app for the broadcast job?
No. Run it as a background task in the same process — a second app can't share your volume anyway.
Wrapping up
The rate limits are the visible problem and the easy one: sleep between sends, back off when told, and you'll never see a 429 that matters. The problem worth designing for is that a broadcast large enough to need pacing is also long enough to outlive your container, and the platform will recreate that container the moment you deploy. Write each send down as it happens, resume from the table on startup, and mark blocked users as you find them. Then a broadcast is something you kick off and forget about, rather than something you have to babysit and hope nobody ships during.