A Telegram bot may work correctly during development but begin returning errors when it sends notifications to many users. One of the most common responses is 429 Too Many Requests, which means the bot has exceeded a flood-control limit.
The correct solution is not to resend the same request continuously. A reliable bot should queue outgoing messages, control its sending speed, read Telegram’s retry_after value, and retry failed requests after the required delay.
This tutorial demonstrates how to build a basic rate-limited Telegram message sender with Python.
Understanding Telegram Bot Rate Limits
Telegram applies different restrictions depending on the destination and sending pattern. According to the official Telegram Bots FAQ, developers should generally avoid sending more than one message per second to a single chat. Group messages and bulk broadcasts have separate limits.
When a bot exceeds a limit, the API may return a response similar to this:
{
"ok": false,
"error_code": 429,
"description": "Too Many Requests: retry after 3",
"parameters": {
"retry_after": 3
}
}
The retry_after field specifies how many seconds the program should wait before repeating the request.
Preparing the Python Project
Create a project directory and install the requests package:
mkdir telegram-rate-limiter
cd telegram-rate-limiter
python -m venv .venv
source .venv/bin/activate
pip install requests
Windows PowerShell users can activate the environment with:
.venv\Scripts\Activate.ps1
Create a bot through BotFather and store its Token in an environment variable. Never publish a bot Token in an article, screenshot or public repository.
Linux and macOS users can run:
export TELEGRAM_BOT_TOKEN="YOUR_BOT_TOKEN"
Windows PowerShell users can run:
$env:TELEGRAM_BOT_TOKEN="YOUR_BOT_TOKEN"
For local testing, install a mobile client or review the available desktop versions through this Telegram download page.
Creating the Telegram API Client
Create a file named sender.py:
import os
import requests
TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
API_URL = f"https://api.telegram.org/bot{TOKEN}"
def call_send_message(chat_id: int, text: str):
response = requests.post(
f"{API_URL}/sendMessage",
json={
"chat_id": chat_id,
"text": text
},
timeout=20
)
try:
data = response.json()
except ValueError:
data = {
"ok": False,
"description": response.text
}
return response.status_code, data
This function returns the HTTP status code and Telegram’s JSON response. The program can therefore distinguish rate limits from network errors and invalid requests.
Adding an Outgoing Message Queue
A queue separates message creation from delivery. Other parts of the application can add messages immediately, while a worker sends them at a controlled speed.
Add the following code:
from dataclasses import dataclass
from queue import Queue
@dataclass
class MessageJob:
chat_id: int
text: str
attempts: int = 0
message_queue = Queue()
A new message can now be scheduled without sending it immediately:
message_queue.put(
MessageJob(
chat_id=123456789,
text="Your report is ready."
)
)
The queue preserves the order of the jobs and allows the sending process to operate independently.
Handling HTTP 429 Responses
Import time and create a function that processes each queued job:
import time
def process_job(job: MessageJob):
status, data = call_send_message(
job.chat_id,
job.text
)
if status == 200 and data.get("ok"):
print(f"Sent message to {job.chat_id}")
return
if status == 429:
parameters = data.get("parameters", {})
wait_time = int(
parameters.get("retry_after", 1)
)
print(
f"Rate limited for {wait_time} seconds. "
f"Chat: {job.chat_id}"
)
time.sleep(wait_time)
message_queue.put(job)
return
raise RuntimeError(
f"Telegram API error: {status} {data}"
)
When Telegram returns HTTP 429, the function reads retry_after, waits for the specified period and returns the job to the queue. This is safer than retrying immediately or using the same arbitrary delay for every error.
Creating the Background Worker
The queue needs a worker that continuously processes pending jobs:
import threading
GLOBAL_INTERVAL = 0.05
def run_worker():
while True:
job = message_queue.get()
try:
process_job(job)
except requests.RequestException as error:
print(f"Network error: {error}")
if job.attempts < 3:
job.attempts += 1
time.sleep(2 ** job.attempts)
message_queue.put(job)
except RuntimeError as error:
print(error)
finally:
message_queue.task_done()
time.sleep(GLOBAL_INTERVAL)
worker = threading.Thread(
target=run_worker,
daemon=True
)
worker.start()
The worker uses exponential backoff for temporary network errors. The first retry waits two seconds, the next waits four seconds, and the final retry waits eight seconds.
Preventing Bursts to the Same Chat
A bot may send messages to different users successfully while still exceeding the limit for one specific conversation. Store the latest delivery time for every chat:
last_sent_at = {}
PER_CHAT_INTERVAL = 1.0
def wait_for_chat(chat_id: int):
previous = last_sent_at.get(chat_id, 0)
elapsed = time.monotonic() - previous
remaining = PER_CHAT_INTERVAL - elapsed
if remaining > 0:
time.sleep(remaining)
last_sent_at[chat_id] = time.monotonic()
Call this function before making the request:
def process_job(job: MessageJob):
wait_for_chat(job.chat_id)
status, data = call_send_message(
job.chat_id,
job.text
)
if status == 200 and data.get("ok"):
print(f"Sent message to {job.chat_id}")
return
if status == 429:
wait_time = int(
data.get("parameters", {})
.get("retry_after", 1)
)
time.sleep(wait_time)
message_queue.put(job)
return
raise RuntimeError(
f"Telegram API error: {status} {data}"
)
For an application with multiple worker processes, per-chat timestamps should be stored in a shared system such as Redis.
Testing the Queue
Add several test jobs at the bottom of the file:
if __name__ == "__main__":
target_chat_id = 123456789
for number in range(1, 6):
message_queue.put(
MessageJob(
chat_id=target_chat_id,
text=f"Queued message number {number}"
)
)
message_queue.join()
print("All jobs completed")
Replace target_chat_id with your own chat ID, start a conversation with the bot and run:
python sender.py
The messages should be delivered in order instead of being sent as one uncontrolled burst.
Recommended Production Improvements
This example stores jobs in memory. If the Python process stops, pending messages will be lost. A production application should use a persistent queue such as Redis, RabbitMQ, Amazon SQS or a database-backed job system.
It should also include:
- Structured logging without exposing the bot Token
- A maximum retry count for permanent failures
- Separate handling for blocked users and invalid chat IDs
- Graceful shutdown logic
- Monitoring for queue size and delivery latency
- Message identifiers to prevent duplicate delivery
The Telegram Bot API documentation should remain the primary source for current methods, parameters and error responses.
Conclusion
Handling Telegram rate limits requires more than adding a delay to a loop. A stable bot needs an outgoing queue, per-chat pacing, network-error retries and explicit handling of HTTP 429 responses.
Once these controls are in place, the same architecture can support monitoring alerts, order updates, scheduled notifications and larger subscriber lists without repeatedly failing during traffic spikes.
Top comments (0)