Rate Limiting¶
A wzgram extension
Telegram answers a client that sends too fast with a FloodWait: a number of seconds you
must not send in. wzgram handles those when they arrive — but a limiter that never lets you
get there is cheaper than one that recovers afterwards, because a flood wait charges for
the request that triggered it too.
Every client has one. Client builds a
RateLimiter with sane defaults whether or not you ask for it, and
invoke() — the choke point every raw call passes through — acquires
from it before the request goes out.
Categories¶
Telegram does not enforce one limit; it enforces different ones for different work. The limiter mirrors that with a token bucket per category and one global bucket over all of them:
Category |
Rate/s |
Burst |
Covers |
|---|---|---|---|
|
20 |
30 |
sending, editing, forwarding text |
|
5 |
10 |
anything carrying a photo, video, audio, file |
|
30 |
50 |
|
|
15 |
20 |
bans, promotions, pins, toggles, deletes |
|
3 |
5 |
update-difference fetches, pings |
|
10 |
15 |
account-level changes |
|
30 |
40 |
everything, on top of its own category |
A request is classified from the name of the raw function it wraps, so a new method lands in the right bucket without anything being registered by hand.
Tuning it¶
Pass rate_limits to the client. Only the categories you name are changed:
app = Client(
"my_bot",
rate_limits={
"media": {"rate": 2, "burst": 4},
"global": {"rate": 15, "burst": 20},
},
)
Lower is slower and safer. The defaults are already below what Telegram publishes, so raising them is how you get flood waits.
Reading the pressure¶
print(app.rate_limiter.congestion()) # 0.0 idle … 1.0 saturated
print(app.rate_limiter.available) # tokens left per category
congestion() is the worst bucket, not the average — it answers “am I about to be
throttled”, which is the question worth asking. A long-running job can use it to back off
before the limiter has to make it wait.
acquire_nowait() takes a token only if one is free and reports whether it did, for work that would rather be skipped
than delayed. update_limits changes the limits on a live client.
How the bucket behaves¶
Two properties are load-bearing, and both were bugs before they were properties:
The wait is served holding the lock. Sleeping outside it woke every waiter at once for a single token, and a deficit of a fraction of a token makes that sleep microseconds long — a spin, under exactly the load the limiter exists for. Five waiters cost twenty sleeps.
Admission is in arrival order.
asyncio.Lockhands over in order, so holding it across the sleep also means a waiter cannot be starved by later arrivals.
Gotchas¶
The limiter is per client. Two clients in one process do not share buckets, so N clients on one account can still flood — lower the limits rather than assuming they compose.
It bounds requests per second, not bytes. A slow uploader is not what it protects against; that is Performance and Resource Budgets.
sleep_thresholdon the client is the other half of this: it decides how long aFloodWaitwzgram will sit out for you rather than raise. The limiter tries to keep you from ever finding out.