@yaebal/ratelimiter
drop incoming updates from users who exceed a configurable rate — a sliding fixed-window counter, keyed per user by default.
install
pnpm add @yaebal/ratelimiterusage
call bot.install(ratelimiter()) once, before any handlers you want protected.
updates that exceed the limit are silently dropped (or handled by onLimit);
updates within the limit pass through to the next middleware.
import { Bot } from "@yaebal/core";
import { ratelimiter } from "@yaebal/ratelimiter";
const bot = new Bot(process.env.BOT_TOKEN!);
// drop updates from users sending more than 5 per second
bot.install(ratelimiter());
bot.on("message:text", (ctx) => ctx.reply("hello!"));
bot.start();options
bot.install(ratelimiter({
limit: 2, // max 2 updates per window
windowMs: 10_000, // per 10-second window
onLimit: async (ctx) => {
await ctx.reply("slow down!");
},
}));custom key
the default key is ctx.from?.id (per-user). supply getKey to
change the unit of limiting. returning undefined from getKey bypasses the limiter for that update entirely.
// rate-limit per chat instead of per user
bot.install(ratelimiter({
getKey: (ctx) => ctx.chat?.id?.toString(),
}));api
| export | signature | description |
|---|---|---|
ratelimiter | (options?: RateLimiterOptions) => Plugin<Context, Record<never, never>> | returns a plugin to pass to bot.install() |
decide | (rec, now, limit, windowMs) => \{ allowed: boolean; window: Window \} | pure window-decision function — exported for testing |
RateLimiterOptions | interface | options bag passed to ratelimiter() |
RateLimiterOptions
| field | type | default | description |
|---|---|---|---|
limit | number | 5 | max updates allowed per window |
windowMs | number | 1000 | window length in milliseconds |
onLimit | (ctx: Context) => unknown | undefined | called when an update is dropped; use to send a warning reply |
getKey | (ctx: Context) => string | undefined | per-user (ctx.from?.id) | return undefined to bypass the limiter for that update |
decide()
the core logic is a pure function with no I/O — exported so you can unit-test custom
policies without a live bot. it takes the existing window record (or undefined for the first call), the current timestamp, the limit, and the window length.
import { decide } from "@yaebal/ratelimiter";
// first two calls allowed, third is blocked
const a = decide(undefined, 1000, 2, 1000);
// => { allowed: true, window: { count: 1, resetAt: 2000 } }
const b = decide(a.window, 1000, 2, 1000);
// => { allowed: true, window: { count: 2, resetAt: 2000 } }
const c = decide(b.window, 1000, 2, 1000);
// => { allowed: false, window: { count: 3, resetAt: 2000 } }
// after the window elapses the counter resets
const over = { count: 9, resetAt: 1500 };
decide(over, 2000, 2, 1000);
// => { allowed: true, window: { count: 1, resetAt: 3000 } }bot.install(), not bot.use(). the ratelimiter
returns a plugin and must be registered with bot.install(ratelimiter()). install before your handlers. the limiter is middleware — updates are dropped before they reach any handlers registered after it. installing it last protects nothing.
the window map is never evicted. one entry per distinct key is held in memory for the lifetime of the process. for bots with a bounded and known user set this is fine; for bots with unbounded growth you may want to add a periodic sweep.
updates without a key are always allowed. if
getKey returns undefined (e.g. updates with no from field, or channel posts
with a custom key function), the limiter skips them — they are not counted or blocked.