rate limits

avoid 429s by separating inbound abuse control, outbound scheduling and retry.

the three layers

layerpackagejob
inbound@yaebal/ratelimiterdrop spammy users before expensive handlers run
outbound@yaebal/throttleschedule telegram api calls through safe buckets
retry@yaebal/againhonor 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.

rate-limit-stack.ts
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:

terminal
pnpm create yaebal my-bot --plugins ratelimiter,throttle,again

safe 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 for loop — 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.

paid-broadcast.ts
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

errorresponse
429 retry_aftersleep, retry, and slow the queue — autoRetry and throttle both read retry_after automatically
403 forbiddenskip permanently and mark recipient inactive
400 bad requestusually a payload bug; fail the job and alert
5xx / networkbounded retry with exponential backoff