Telegram Bot API Errors: Which Ones to Retry and Which to Never Retry
Telegram Bot API Errors: Which Ones to Retry and Which to Never Retry
Most Telegram Bot API error codes get the same handling: one try/except that retries everything. That is worse than no retry logic, because it turns permanent failures into infinite loops and temporary throttling into escalating throttling.
The useful distinction is not the error code. It is: will this ever succeed if I try again?
The error Telegram tells you how to retry
429 — Too Many Requests.
This is the error where retrying is not just correct but scheduled for you. When it is flood control, the error's parameters object (ResponseParameters) carries retry_after — "Optional. In case of exceeding flood control, the number of seconds left to wait before the request can be repeated". Both parameters and retry_after are Optional, and not every 429 is flood control (close returns one in a bot's first 10 minutes), so treat it as a hint that may be absent. 5xx is the other retryable class; see below.
on 429:
wait(retry_after or DEFAULT_BACKOFF) # Telegram's number when it is there
retry once
Three ways people get this wrong:
Retrying immediately. Deepens the throttle. The most common mistake.
Using their own backoff. Telegram gave you the number. A fixed 5-second backoff is either too slow or gets you throttled again.
Retrying indefinitely. Two attempts, then queue it or drop it. Sustained 429s mean your send rate is wrong — see scaling past the rate limits.
Errors you must never retry
None of these will succeed on an immediate retry, and most will not succeed on any retry. Retrying them burns send capacity and backs your queue up behind messages that will not go out. Two are reversible on a human timescale — a block and a kick — so record them as inactive rather than deleting them, and let my_chat_member bring them back.
The user blocked your bot. The most important one to handle. It is durable but not permanent — a user can unblock you, and Telegram tells you when they do. For private chats, my_chat_member "is received only when the bot is blocked or unblocked by the user", and it is in the default update set (only chat_member, message_reaction and message_reaction_count need an explicit allowed_updates opt-in). Mark them inactive and stop sending, but reactivate on unblock rather than treating the row as dead. A queue that retries blocked users forever gradually fills with them.
This is also your unsubscribe signal — Telegram has no unsubscribe link, so a block is how people leave. Count them per campaign; they measure lost reach, some of which comes back.
Chat not found. Wrong ID, deleted chat, or the bot was removed. Not transient.
Bot was kicked from the group. Stop sending to that chat and update your records.
Invalid token. Every call fails. Usually means the token was regenerated in @BotFather. Loud and immediate.
Message to edit not found / message is not modified. Editing something deleted, or editing to identical content. The second is worth catching separately — it usually means a bug in your own state tracking, not a real error.
Bad request from malformed input. Text too long, invalid parse mode, wrong parameter type. Fix the input; retrying identical malformed input yields an identical error.
Group migrated to a supergroup. The exception in this list — not a permanent failure. The error carries parameters.migrate_to_chat_id, "The group has been migrated to a supergroup with the specified identifier". Persist the new ID (the docs note it can exceed 32 significant bits but has at most 52, so store it in a signed 64-bit integer) and retry once against it.
The gap between 4xx and 5xx
4xx errors are usually your problem. Something about the request is wrong, and the same request will usually fail the same way. Do not blindly retry — with two exceptions: 429, and a group that has migrated to a supergroup.
5xx errors are Telegram's problem and are usually transient. A small number of retries with backoff is reasonable here.
The one that isn't an error
A non-2XX response from your webhook. This is the reverse direction and easy to overlook.
From the documentation: "In case of an unsuccessful request (a request with response HTTP status code different from 2XY), we will repeat the request and give up after a reasonable amount of attempts."
So when your endpoint returns 500, Telegram repeats the request and eventually gives up. The update is not thrown away at that moment — updates are "stored on the server until the bot receives them either way, but they will not be kept longer than 24 hours". The backlog is visible in getWebhookInfo.pending_update_count, and you can clear it deliberately with deleteWebhook's drop_pending_updates. Past 24 hours it is genuinely gone.
This is why a deploy that returns 502 for a sustained period puts updates at risk. Telegram does not publish how many times it retries or over what window — only that it will "repeat the request and give up after a reasonable amount of attempts" — so treat the safe window as unknown and the 24-hour retention cap as the real deadline. It is not silent either: getWebhookInfo exposes pending_update_count, last_error_date and last_error_message. Return 200 immediately, process asynchronously, and never let your response depend on work that can fail.
A handler that works
try:
send(...)
except TelegramError as e:
if e.code == 429:
wait(e.retry_after or 5); retry_once()
elif user_blocked_bot(e) or chat_gone(e):
mark_inactive(user_id) # stop sending; revive on my_chat_member
elif e.code >= 500:
backoff_retry(max=3) # transient
else:
log_and_drop(e) # your bug; fix the input
Four branches. Most production bots need nothing more.
What to actually monitor
Block rate per campaign. Lost reach — recoverable when users unblock — and the sharpest negative feedback you get.
429 rate. Occasional is fine. Sustained means your sending architecture needs a shared rate limiter.
5xx rate from Telegram. Spikes are usually Telegram-side; check @BotNews before debugging your own code.
Non-2XX rate from your own webhook. Every one of these is an update you have not processed yet; past 24 hours it is unrecoverable.
getWebhookInfo pending_update_count and last_error_date. Telegram's own view of what it could not deliver. A backlog with a recent error timestamp is the alarm that matters.
Dead-letter queue depth. If permanent failures are not being separated out, they are clogging your retry path.
A note on error codes and error strings
Neither half of a Telegram error is fully specified. The docs say the error "is explained in the 'description'", and that the error_code field's "contents are subject to change in the future". The Bot API reference publishes no table of numeric codes at all, and no list of description strings.
Do not match on exact error strings.* Normalise before comparing — lowercase, substring, never equality — and treat an unrecognised description as a loud unknown rather than a silent default. You will need description matching: the code alone does not separate a blocked user from a bot that was kicked, since both surface as 403. Where Telegram gives you a machine-readable signal, prefer it — parameters (ResponseParameters) is the documented way "to automatically handle the error"*. Code that branches on a specific phrase will break the day that phrase is reworded, and it will break silently.
retry_after and webhook retry behaviour quoted from Telegram's Bot API documentation, verified September 2026. Specific error descriptions are not exhaustively documented by Telegram; the categories above reflect documented behaviour plus widely observed practice.