rate limits
avoid 429s by separating inbound abuse control, outbound scheduling and retry.
the three layers
| layer | package | job |
|---|---|---|
| inbound | @yaebal/ratelimiter | drop spammy users before expensive handlers run |
| outbound | @yaebal/throttle | schedule telegram api calls through safe buckets |
| retry | @yaebal/again | honor retry_after and transient 5xx/network failures |
installation order here doesn't matter between these three specifically — throttle and autoRetry hook into bot.api directly (not the middleware chain), and ratelimiter only needs to run before your handlers, which it does as long as it's
installed before them.
import { createBot } from "yaebal";
import { autoRetry } from "@yaebal/again";
import { throttle } from "@yaebal/throttle";
import { ratelimiter } from "@yaebal/ratelimiter";
export const bot = createBot(process.env.BOT_TOKEN!)
.install(ratelimiter({ limit: 5, windowMs: 1000 }))
.install(throttle({ globalPerSec: 25, perChatPerSec: 1, perGroupPerMin: 20 }))
.install(autoRetry({ maxRetries: 5, maxDelayMs: 30_000, retryAfterPaddingMs: 250 }));create-yaebal can wire all three in at scaffold time instead of installing them by hand:
pnpm create yaebal my-bot --plugins ratelimiter,throttle,againsafe defaults
- treat 30 messages/sec bot-wide as a ceiling, not a target — that's
throttle's own default. - keep one private chat near 1 message/sec unless you have a specific reason.
- serialize per-chat updates when handlers mutate state — see @yaebal/runner for polling at scale.
- for broadcasts, use a durable queue instead of a raw
forloop — see queues and broadcasts. - always record 403 recipients and remove them from future campaigns.
paid broadcast
telegram supports allow_paid_broadcast for high-throughput paid sends, bypassing the
usual per-chat pacing. still keep retry and accounting: stars balance, failed recipients and
permanent skips are business data either way.
import { createBot } from "yaebal";
const bot = createBot(process.env.BOT_TOKEN!);
const chatId = 123456789;
const text = "campaign message";
await bot.api.call("sendMessage", {
chat_id: chatId,
text,
allow_paid_broadcast: true,
});failure policy
| error | response |
|---|---|
429 retry_after | sleep, retry, and slow the queue — autoRetry and throttle both read retry_after automatically |
403 forbidden | skip permanently and mark recipient inactive |
400 bad request | usually a payload bug; fail the job and alert |
5xx / network | bounded retry with exponential backoff |