Telegraft

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)
  }
}
The gate is shared state rather than a local variable, which is the difference between one 429 causing one pause and one 429 causing a storm across every worker.

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