production
a practical checklist for running yaebal bots under real traffic: retries, rate limits, concurrency, webhooks, shutdown, tests, and observability.
the baseline stack
most production bots need three guards: retry transient telegram failures, throttle outgoing calls to avoid 429s, and rate-limit abusive users before expensive handlers run.
import { createBot } from "yaebal";
import { autoRetry } from "@yaebal/again";
import { throttle } from "@yaebal/throttle";
import { ratelimiter } from "@yaebal/ratelimiter";
const bot = createBot(process.env.BOT_TOKEN!)
.install(throttle({ globalPerSec: 30, perChatPerSec: 1, perGroupPerMin: 20 }))
.install(autoRetry({ maxRetries: 5, maxDelayMs: 10_000, retryAfterPaddingMs: 250 }))
.install(ratelimiter({ limit: 5, windowMs: 1000 }));| piece | why |
|---|---|
@yaebal/again | await structured retry_after waits and transient 5xx errors |
@yaebal/throttle | schedule outgoing API calls through global/private/group buckets |
@yaebal/ratelimiter | drop spammy incoming updates before they hit business logic |
polling at scale
plain bot.start() processes updates sequentially. use @yaebal/runner when one slow handler should not block unrelated chats. keep per-chat ordering enabled if handlers
read and write session state.
import { run, chatKey } from "@yaebal/runner";
const handle = run(bot, {
concurrency: 50,
sequentializeBy: chatKey,
allowedUpdates: ["message", "callback_query"],
onError: (error, update) => {
console.error("update failed", update?.update_id, error);
},
});
process.once("SIGTERM", () => void handle.stop());chatKey serializes each chat while unrelated chats
still run in parallel.webhooks for serverless and edge
for cloudflare workers, deno deploy, vercel edge, and similar runtimes, use a fetch-style webhook. the handler validates the secret token in constant time and rejects oversized bodies.
import { createBot, webhook } from "yaebal";
const bot = createBot(env.BOT_TOKEN);
bot.command("start", (ctx) => ctx.reply("running"));
export default {
fetch: webhook(bot, { secretToken: env.WEBHOOK_SECRET }),
};register the webhook during deploy with @yaebal/web or the raw bot api. keep the secret token in your host's secret store, not in source.
graceful shutdown
on polling deployments, catch SIGTERM and let the bot stop cleanly. with runner, handle.stop() stops polling and drains in-flight updates. with plain polling, bot.stop() stops the loop.
let stopping = false;
process.once("SIGTERM", async () => {
if (stopping) return;
stopping = true;
await bot.stop();
});observability
use api hooks for request-level logs/metrics and bot.onError for handler failures.
do not log tokens, file download urls, raw secrets, or full user payloads in production logs.
bot.api.before((method, params) => {
console.log("telegram ->", method);
return params;
});
bot.api.after((method) => {
console.log("telegram <-", method, "ok");
});
bot.onError((error, ctx) => {
console.error("handler failed", ctx.update.update_id, error);
});broadcasts
for large audiences, use @yaebal/broadcast with persistent storage, bounded retry and an explicit rate limit. keep skipped recipients for
cleanup, and remove users that blocked the bot when telegram returns 403.
state and storage
| state | production advice |
|---|---|
| sessions | use persistent storage for anything that must survive restarts; memory storage is dev-only |
| scenes/conversations | prefer durable state for long flows; short prompt-style flows can be in-memory |
| media cache | cache file ids so repeated sends do not re-upload the same bytes |
pre-release checklist
- run
pnpm typecheckand your bot tests. - exercise critical flows with @yaebal/test.
- set
allowedUpdatesexplicitly for the update kinds you use. - use webhook secret tokens in production.
- keep one polling process per token, or use webhooks.
- set a broadcast rate limit and storage adapter before large outbound flows.
- document how to rotate the bot token and webhook secret.