Telegram broadcast bot
Collect subscribers and send them announcements without tripping Telegram's rate limits.
- Node.js
- Telegram Bot API
- SQLite
Telugu lo nerchuko · Watch in Telugu
Telegram is a fast way to reach an audience, and its Bot API is friendlier than WhatsApp's — no business verification to get started. This project builds a bot that grows a subscriber list and sends announcements to everyone, with the rate-limit handling that separates a working broadcaster from a banned one.
Overview
You will build a bot that lets people subscribe with /start, stores their chat IDs, and broadcasts a message from an admin command to the whole list. The real lesson is throttling: Telegram limits how fast you can send, and ignoring that gets your bot rate-limited or blocked.
The problem
Announcing something to many people one chat at a time is impossible by hand, and a naive loop that fires all messages at once hits Telegram's limits immediately. You need a list you can grow, an admin-only way to broadcast, and a sender that paces itself and survives users who blocked the bot.
Architecture
The bot long-polls or receives webhook updates from Telegram. Subscriptions are stored in SQLite. A broadcast command iterates the list with a throttled sender that handles errors per recipient.
Users --> /start --> bot --> SQLite (subscribers)
Admin --> /broadcast <msg> --> throttled sender --> Telegram API --> all usersTech stack
- Node.js with a Telegram Bot API client
- SQLite for the subscriber list (one file, zero setup)
- A small token-bucket or fixed-delay throttle for sending
Build steps
- Handle subscribe and unsubscribe.
import TelegramBot from "node-telegram-bot-api";
import Database from "better-sqlite3";
const db = new Database("subs.db");
db.exec("CREATE TABLE IF NOT EXISTS subs (chat_id INTEGER PRIMARY KEY)");
const bot = new TelegramBot(process.env.TG_TOKEN, { polling: true });
bot.onText(/\/start/, (m) => {
db.prepare("INSERT OR IGNORE INTO subs (chat_id) VALUES (?)").run(m.chat.id);
bot.sendMessage(m.chat.id, "Subscribed. You will get our announcements here.");
});
bot.onText(/\/stop/, (m) => {
db.prepare("DELETE FROM subs WHERE chat_id = ?").run(m.chat.id);
bot.sendMessage(m.chat.id, "Unsubscribed.");
});- Gate the broadcast command to admins only.
const ADMINS = new Set((process.env.TG_ADMINS ?? "").split(",").map(Number));
bot.onText(/\/broadcast (.+)/s, async (m, match) => {
if (!ADMINS.has(m.from.id)) return bot.sendMessage(m.chat.id, "Not allowed.");
const text = match[1];
const targets = db.prepare("SELECT chat_id FROM subs").all();
await broadcast(targets.map((r) => r.chat_id), text);
bot.sendMessage(m.chat.id, `Sent to ${targets.length} subscribers.`);
});- Send with throttling and per-user error handling.
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function broadcast(chatIds, text) {
for (const id of chatIds) {
try {
await bot.sendMessage(id, text);
} catch (err) {
// 403 = user blocked the bot; prune them
if (err.response?.statusCode === 403) {
db.prepare("DELETE FROM subs WHERE chat_id = ?").run(id);
}
}
await sleep(50); // ~20 msgs/sec, under Telegram's broadcast limit
}
}Source code
Starter with the schema, throttle, and admin gate: AnythingWithSandy/tg-broadcast-bot.
Demo
Create a bot with @BotFather, set TG_TOKEN, and message your own bot to test subscribe and broadcast. No shared demo, since each bot needs its own token.
Deployment
Run the process on any always-on host (a small VPS or a process manager like pm2). Keep TG_TOKEN and TG_ADMINS in environment secrets. For higher volume, switch from polling to webhooks.
Bonus challenge
Add scheduled broadcasts (a cron that sends a saved message at a set time), delivery stats (sent, failed, pruned), and an LLM step that drafts the announcement from a one-line brief before the admin approves and sends it.