Bot API reference
What Telegram does when you send too fast
Exceeding a Telegram rate limit returns HTTP 429 with a JSON body containing `parameters.retry_after`, an integer number of seconds. That value is an instruction, not an estimate: waiting less escalates the penalty, and waiting the stated interval before retrying the same request is the whole of the correct handling.
retry_after: the exact figures
- Response on exceeding a limit
- HTTP 429 with parameters.retry_after in whole seconds
- Status code
- HTTP 429
- Field to read
- parameters.retry_after, integer seconds
- Correct backoff
- Exactly the stated interval — never a shorter guess
- Scope of the pause
- The whole send queue, not the individual request
As of 2025-10-01, Telegram Bot API 13.4
What it means in practice
The response carries everything needed to recover. `ok` is false, `error_code` is 429, `description` reads "Too Many Requests: retry after N", and `parameters.retry_after` holds N as an integer. Any client library worth using surfaces that field; if the one in use does not, it is worth reading the raw error rather than falling back on a generic exponential backoff, because a guessed interval is exactly what turns a two-second pause into a two-minute one.
The most damaging mistake here is a well-intentioned one. A generic HTTP client configured with exponential backoff and jitter will typically retry after a fraction of a second, which is far shorter than the interval Telegram just named. The server sees a client ignoring its instruction and extends the wait; the client backs off slightly further and retries again; the bot spends minutes locked out of an API it could have used after two seconds. Standard retry policy is actively wrong against this API and must be replaced rather than tuned.
The wait should apply to the queue rather than to the request. Pausing only the failed call leaves every other worker and every other queued message hammering the same budget, which reproduces the condition immediately. Setting a shared "not before" timestamp that every sender checks turns one 429 into one pause; handling it per-request turns it into a storm.
Idempotency matters at the retry boundary and is easy to get wrong. A 429 means the request was rejected, so a retry is safe — but a timeout is a different failure, where the message may well have been delivered before the connection dropped. Retrying a timeout without a deduplication key is how subscribers receive a broadcast twice, and the fix belongs in the queue: record the send as attempted before the call, and reconcile after.
Some 429s are informational rather than punitive. Telegram throttles a chat that a bot is flooding even when the bot is well under its own budget, and the retry_after there reflects the recipient chat rather than the bot. The handling is identical, but the diagnosis differs: logging the chat id alongside every 429 is what separates "we are sending too fast overall" from "this one group is saturated".
Handling it in code
// One shared gate, honoured by every sender. Not a per-request backoff.
let notBefore = 0
function retryAfterOf(error: unknown): number | undefined {
const p = (error as { parameters?: { retry_after?: number } }).parameters
return p?.retry_after
}
async function callWithFloodControl<T>(fn: () => Promise<T>): Promise<T> {
const wait = notBefore - Date.now()
if (wait > 0) await new Promise((r) => setTimeout(r, wait))
try {
return await fn()
} catch (error) {
const retryAfter = retryAfterOf(error)
if (retryAfter === undefined) throw error
// The server named the interval. Use it verbatim, and hold every other sender too.
notBefore = Date.now() + retryAfter * 1000
return callWithFloodControl(fn)
}
}Questions this raises
Should I add jitter to the retry_after value?
Add to it, never subtract. Jitter exists to stop many clients retrying in lockstep, which is a real concern when several workers share a token — but it must only ever push the retry later than the interval Telegram named. Jitter that can shorten the wait is a bug wearing the costume of a best practice.
Does a 429 mean the message was not sent?
Yes. A 429 is a rejection, so retrying the same request is safe and will not duplicate anything. The dangerous case is a network timeout, where the message may already have been delivered — that is the failure mode that needs a deduplication key, not this one.
Why am I getting 429s when I am well under 30 per second?
Because per-chat throttling is separate from your own budget. Telegram limits how fast a bot may post into one chat regardless of what the bot is doing elsewhere, so a burst into a single busy group produces 429s while the global rate looks comfortable. Logging the chat id with each 429 makes the distinction immediate.
Can retry_after be minutes rather than seconds?
Yes, and that is almost always a symptom of having ignored earlier, shorter waits. The value grows with repeated violations, which is why the first two-second wait is worth honouring precisely — it is the cheapest the penalty will ever be.
Related limits
How the bot receives updates, and why that shapes where the retry gate belongs, is covered in webhooks versus long polling.
For validating that an incoming update really came from Telegram, see the webhook secret token.
The related idempotency problem on the inbound side is described in at-least-once update delivery.
The limit most often responsible for a 429 during a campaign is the bot-wide send ceiling.
A queue that implements this gate, survives restarts and never double-sends is specified in the dispatch build.