REPOSITORY / ScuroNeko/Laniakea

Wiki

KNOWLEDGE REPOSITORY
3
Rate Limiting
ScuroNeko edited this page 2026-03-26 23:05:57 +03:00

Rate Limiting

Russian version: Rate-Limiting-RU

This page explains how Laniakea throttles outgoing Telegram API requests and how it reacts when Telegram answers with 429 Too Many Requests. The built-in limiter is designed to protect both the global bot throughput and hot chats that might otherwise overwhelm the API.

Overview

Laniakea wires a RateLimiter into the internal tgapi.API client during NewBot(...).

That limiter combines:

  • a global token bucket;
  • per-chat token buckets;
  • global cooldown locks;
  • per-chat cooldown locks.

In other words, the library handles both steady-state throttling and reactive backoff after Telegram explicitly says to wait.

Default behavior

The built-in limiter starts with these defaults:

  • global limit: 30 requests per second, burst 30;
  • per-chat limit: 1 request per second, burst 1.

When a bot is created, BotOpts.RateLimit can override the global rate. The per-chat limiter remains 1 req/s per chat in the current implementation.

Where the limiter applies

The limiter is used by:

  • normal JSON Telegram API requests through tgapi.API;
  • multipart uploader requests through tgapi.Uploader.

That matters because rate limiting is not only about SendMessage(...). Upload-heavy bots still benefit from the same cooldown handling and retry behavior.

Two operating modes

The limiter supports two modes:

  • wait mode;
  • drop mode.

The mode is controlled by BotOpts.DropRLOverflow and passed into tgapi.API as the limiter's overflow behavior.

Wait mode

Wait mode is the default and usually the safest option.

In wait mode:

  • if capacity is available, the request proceeds immediately;
  • if a limiter bucket is empty, the request waits;
  • if a global or chat cooldown lock is active, the request waits until that lock expires;
  • if the context is canceled while waiting, the request returns the context error.

This mode favors reliability and delivery over latency.

It is usually the right choice for:

  • bots where losing messages is unacceptable;
  • admin or workflow bots;
  • bots that send important transactional responses.

Drop mode

Drop mode rejects requests immediately when a limiter would otherwise block.

In drop mode:

  • requests do not wait for limiter capacity;
  • requests do not wait for cooldown locks to expire;
  • the limiter returns ErrDropOverflow instead.

This mode favors responsiveness over guaranteed delivery.

It can make sense for:

  • noisy bots with low-value updates;
  • bots where stale replies are worse than skipped replies;
  • telemetry or best-effort notification workloads.

Be careful with it in user-facing command flows, because it can turn load spikes into visible dropped messages.

Global limit versus per-chat limit

The limiter checks both global and per-chat constraints.

Global limit protects the bot as a whole:

  • too many concurrent requests across all chats will hit the global bucket first.

Per-chat limit protects one chat from becoming too noisy:

  • a flood in one chat does not automatically consume the entire chat-level budget of another chat;
  • chat cooldowns are scoped to the affected chat.

This is especially helpful for bots used in large groups and private chats at the same time.

How retry_after is handled

When Telegram replies with error 429 and a retry_after value:

  • Laniakea logs the cooldown;
  • the limiter stores a cooldown lock;
  • the lock is scoped to the chat if the request had a chat ID;
  • otherwise the lock becomes global;
  • the client waits for the specified time and retries the request automatically.

This behavior exists in both the normal API client and the uploader path.

That means Telegram's own feedback actively reshapes future request pacing instead of being treated as a plain error.

Chat-scoped versus global cooldowns

If the request is associated with a concrete chatID, Laniakea applies retry_after as a chat-specific lock.

If the request has no chat context, Laniakea applies it as a global lock.

Examples of global-scope requests:

  • requests that are not tied to one chat;
  • some infrastructure or metadata calls;
  • requests created without an associated chat ID in the low-level API.

This distinction is important because it prevents one noisy chat from unnecessarily freezing the entire bot when Telegram's limit is actually chat-local.

Context cancellation behavior

Waiting is always context-aware.

If the bot or request context is canceled while the limiter is waiting:

  • the wait stops immediately;
  • the request returns the context error instead of hanging until the cooldown finishes.

This matters for graceful shutdown, because rate-limited requests should not keep the process alive longer than the caller intends.

Practical tuning

Small and medium bots

Start with the default global rate or a conservative custom value. The defaults are usually good enough unless you already know your workload characteristics.

Bots with bursts across many chats

Increase BotOpts.RateLimit carefully if:

  • handlers are fast;
  • your infrastructure can absorb the parallelism;
  • you are not already seeing Telegram 429 responses.

Do not assume that raising the global limit alone solves everything. Per-chat pressure can still trigger chat-local cooldowns.

Bots with heavy uploads

Remember that uploader requests also participate in rate limiting. If your bot sends media aggressively, watch for retry_after behavior there too, not only in message sends.

Low-value, high-volume bots

Consider drop mode only when skipped messages are acceptable. It is a policy choice, not a performance upgrade in all cases.

Configuration points

The main knobs exposed through BotOpts are:

  • SetRateLimit(limit) for the global request-per-second limit;
  • SetDropRLOverflow(drop) to choose drop mode instead of waiting.

There is no high-level bot option today for changing the per-chat 1 req/s limiter. If you need a different per-chat policy, that would currently require working with the lower-level limiter implementation directly.

Caveats

  • RateLimit <= 0 does not replace the built-in global limiter; it leaves the current limiter settings in place.
  • Drop mode returns ErrDropOverflow, so callers that care about delivery should surface or log that explicitly.
  • retry_after auto-retry still depends on context lifetime; cancellation wins over waiting.
  • Low-level requests created without chat IDs can only benefit from global cooldown scoping, not chat-local scoping.

Recommendations

  • Default to wait mode unless you have a strong reason to prefer dropping.
  • Tune the global limit gradually and based on observed traffic.
  • Expect retry_after to happen occasionally and design handlers to tolerate delayed delivery.
  • For chatty bots, monitor which flows are producing the most requests before raising limits.