DEV Community

How to Handle Telegram Bot Rate Limits with Python

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Windows PowerShell users can activate the environment with:

.venv\Scripts\Activate.ps1
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Windows PowerShell users can run:

$env:TELEGRAM_BOT_TOKEN="YOUR_BOT_TOKEN"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

A new message can now be scheduled without sending it immediately:

message_queue.put(
    MessageJob(
        chat_id=123456789,
        text="Your report is ready."
    )
)
Enter fullscreen mode Exit fullscreen mode

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}"
    )
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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}"
    )
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

Replace target_chat_id with your own chat ID, start a conversation with the bot and run:

python sender.py
Enter fullscreen mode Exit fullscreen mode

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)