Rate limits
Telegram Bot API enforces strict rate limits to protect its infrastructure and other bots from abuse. If your bot sends too many requests in a short period, the API will respond with HTTP 429 — Too Many Requests and a retry_after field indicating how many seconds the bot must wait before making any API calls.
Key limits to know
Telegram does not officially publish exact rate limit numbers. The values below are approximate and come from community experience — they are safe to follow, but the real limits are dynamic and depend on factors like your bot's age, history, and the type of requests.
| Limit | Approximate value |
|---|---|
| Messages to the same chat | ~1 per second |
| Messages to different chats | ~30 per second |
| Bulk notifications / broadcasts | ~30 per second total |
| Group/channel operations | Lower, depends on members |
Paid broadcast (allow_paid_broadcast) | Up to 1000 per second (0.1 Stars/msg) |
NOTE
In practice the limits are more nuanced than a simple "30 per second" — they can change dynamically and may differ between bots. If your bot needs higher throughput, you can contact @BotSupport and request a limit increase for your bot.
TIP
If you're not doing broadcasts — just responding to user messages — you almost certainly won't hit these limits. But it's still good practice to add the auto-retry plugin as a safety net.
What happens when you exceed the limit
When you go over the rate limit, Telegram returns:
{
"ok": false,
"error_code": 429,
"description": "Too Many Requests: retry after 35",
"parameters": {
"retry_after": 35
}
}Your bot is completely blocked for the duration of retry_after (up to 35+ seconds). No API calls will succeed during this time — not just for the users you were broadcasting to, but for all users interacting with your bot.
The problem: broadcasting without delays
A common mistake is iterating over a list of users and sending messages as fast as possible:
// DON'T do this!
for (const chatId of chatIds) {
await bot.api.sendMessage({ chat_id: chatId, text: "Hi!" });
// No delay → instant 429 after ~30 messages
}This fires requests at maximum speed. After roughly 30 messages, Telegram blocks your bot entirely.
for...of loop without any delays fires requests as fast as possible. After ~30 messages per second, Telegram responds with 429 Too Many Requests and blocks your bot entirely. How to broadcast correctly
Paid approach: allow_paid_broadcast
If budget allows, the simplest way to bypass rate limits entirely is the allow_paid_broadcast parameter. Pass true to any send method (e.g. sendMessage) and your bot can send up to 1000 messages per second — at a cost of 0.1 Telegram Stars per message, withdrawn automatically from the bot's balance.
import { Bot } from "gramio";
const bot = new Bot(process.env.BOT_TOKEN as string);
const chatIds: number[] = [
/** some chat ids */
];
for (const chatId of chatIds) {
await bot.api.sendMessage({
chat_id: chatId,
text: "Hi!",
allow_paid_broadcast: true,
});
}TIP
allow_paid_broadcast doesn't replace retry logic — you should still handle transient errors. Combine it with auto-retry for a robust solution.
WARNING
Make sure your bot has enough Stars in its balance before starting a large broadcast. You can check and top up the balance via @BotFather.
Simple approach: loop with delay
Use the built-in withRetries function which catches retry_after errors, waits the specified time, and retries the request automatically. Combine it with a base delay between each request to stay under the limit:
// experimental API available since Node.js@16.14.0
import { scheduler } from "node:timers/promises";
import { Bot, TelegramError } from "gramio";
import { withRetries } from "gramio/utils";
const bot = new Bot(process.env.BOT_TOKEN as string);
const chatIds: number[] = [
/** some chat ids */
];
for (const chatId of chatIds) {
const result = await withRetries(() =>
bot.api.sendMessage({
suppress: true,
chat_id: chatId,
text: "Hi!",
})
);
await scheduler.wait(100); // Base delay between successful requests to avoid rate limits
}This is a good starting point for small broadcasts (up to a few thousand users). But it has downsides:
- Not persistent — if the server restarts, you lose your position in the list
- Not scalable — a single loop can't be parallelized across multiple servers
- No progress tracking — you don't know which users have already received the message
Production-ready: @gramio/broadcast
For real-world broadcasting, use @gramio/broadcast — a queue-based solution built into the GramIO ecosystem. It's persistent (survives restarts), horizontally scalable, and handles rate limiting automatically.
Requirements: Redis
import { Bot, InlineKeyboard } from "gramio";
import Redis from "ioredis";
import { Broadcast } from "@gramio/broadcast";
const redis = new Redis({
maxRetriesPerRequest: null,
});
const bot = new Bot(process.env.BOT_TOKEN as string);
const broadcast = new Broadcast(redis).type("test", (chatId: number) =>
bot.api.sendMessage({
chat_id: chatId,
text: "test",
})
);
console.log("prepared to start");
const chatIds = [617580375];
await broadcast.start(
"test",
chatIds.map((x) => [x])
);
// graceful shutdown
async function gracefulShutdown() {
console.log(`Process ${process.pid} go to sleep`);
await broadcast.job.queue.close();
console.log("closed");
process.exit(0);
}
process.on("SIGTERM", gracefulShutdown);
process.on("SIGINT", gracefulShutdown);You define broadcast types with full type safety — each type specifies what data the broadcast function receives. Then call broadcast.start with an array of arguments and the library handles queuing, rate limiting, retries, and persistence.
Custom implementation with BullMQ
If you need full control over the queue behavior, you can build your own solution using BullMQ with jobify:
Requirements: Redis, ioredis, bullmq, jobify
import { Worker } from "bullmq";
import { Bot, TelegramError } from "gramio";
import { Redis } from "ioredis";
import { initJobify } from "jobify";
const bot = new Bot(process.env.BOT_TOKEN as string);
const redis = new Redis({
maxRetriesPerRequest: null,
});
const defineJob = initJobify(redis);
const text = "Hello, world!";
const sendMailing = defineJob("send-mailing")
.input<{ chatId: number }>()
.options({
limiter: {
max: 20,
duration: 1000,
},
})
.action(async ({ data: { chatId } }) => {
const response = await bot.api.sendMessage({
chat_id: chatId,
suppress: true,
text,
});
if (response instanceof TelegramError) {
if (response.payload?.retry_after) {
await sendMailing.worker.rateLimit(
response.payload.retry_after * 1000
);
// use this only if you did not use auto-retry
// because it re-run this job again
throw Worker.RateLimitError();
} else throw response;
}
});
const chats: number[] = []; // pick chats from database
await sendMailing.addBulk(
chats.map((x) => ({
name: "mailing",
data: {
chatId: x,
},
}))
);The limiter option ensures BullMQ processes at most 20 jobs per second. If a 429 error occurs, the worker pauses for the time specified in retry_after and then resumes.