@yaebal/throttle

space out outgoing API calls to stay within Telegram's rate limits — calls are delayed, never dropped.

install

terminal
pnpm add @yaebal/throttle

usage

call throttle(bot.api) once after constructing the bot. it installs a before hook on the API layer that enforces a minimum gap between consecutive outgoing calls. no middleware registration needed.

bot.ts
import { Bot } from "@yaebal/core";
import { throttle } from "@yaebal/throttle";

const bot = new Bot(process.env.BOT_TOKEN!);

// space outgoing API calls to ≤ 30/sec (Telegram's global cap)
throttle(bot.api);

bot.on("message:text", (ctx) => ctx.reply("hello!"));
bot.start();

custom interval

bot.ts
// tighter limit — one call per 100 ms
throttle(bot.api, { minIntervalMs: 100 });

api

exportsignaturedescription
throttle(api: Api, options?: ThrottleOptions) => voidinstalls the throttle hook on bot.api
reserve(now: number, next: number, interval: number) => \{ at: number; next: number \}pure slot-reservation function — exported for testing
ThrottleOptionsinterfaceoptions bag passed to throttle()

ThrottleOptions

fieldtypedefaultdescription
minIntervalMsnumber34minimum milliseconds between outgoing API calls (~30 calls/sec, Telegram's global cap)

how slot reservation works

reserve(now, next, interval) returns the earliest slot at or after now that is at least interval ms after the previous slot. if the bot is idle, the next call fires at now with no artificial delay. if calls arrive faster than the interval, they queue up by advancing the next-slot pointer.

reserve.ts
import { reserve } from "@yaebal/throttle";

// first call at t=1000 — fires immediately, next slot at 1034
reserve(1000, 0, 34);
// => { at: 1000, next: 1034 }

// second call arrives at t=1000 — queued to slot 1034
reserve(1000, 1034, 34);
// => { at: 1034, next: 1068 }

// call arrives after a long gap — fires immediately at t=2000
reserve(2000, 1068, 34);
// => { at: 2000, next: 2034 }
calls are delayed, not dropped. every API call will eventually go through — throttle only adds a wait, it never discards a request. if your bot sends a large burst it will drain the queue over time rather than losing messages.

this is a global outgoing rate limit. throttle operates on bot.api and applies to every method call regardless of which user triggered it. for per-user incoming-update limiting, use @yaebal/ratelimiter instead.

the default 34 ms corresponds to Telegram's documented global cap of ~30 messages/second. lower values risk 429 errors; the @yaebal/again plugin can handle those automatically if they occur.