-
Notifications
You must be signed in to change notification settings - Fork 0
docs: per-chat resend rate limiter (whatsapp-rust PR #871) #334
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
8e80a93
6d0527d
4f84a1d
d5e1135
bfb2294
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1105,6 +1105,56 @@ pub fn wanted_pre_key_count(&self) -> usize | |
|
|
||
| Returns the currently configured pre-key upload batch size. | ||
|
|
||
| ### set_resend_rate_limit | ||
|
|
||
| ```rust | ||
| pub fn set_resend_rate_limit(&self, burst: u32, refill_per_min: u32) | ||
| ``` | ||
|
|
||
| Retunes the per-chat outbound resend rate limiter live, without reconnecting. | ||
|
|
||
| Outbound retry resends to a group are bounded by a token bucket keyed on the chat JID. `burst` is the instantaneous allowance; `refill_per_min` is the sustained per-chat ceiling. This caps the aggregate resend rate WhatsApp's anti-abuse penalizes during a PN-to-LID migration fan-out. Takes effect on each chat's next retry; a lowered `burst` clamps a live bucket on its next access. A `burst` of `0` disables the limiter entirely. | ||
|
|
||
| <ParamField path="burst" type="u32" required> | ||
| Instantaneous token allowance per chat. Set to `0` to disable. | ||
| </ParamField> | ||
|
|
||
| <ParamField path="refill_per_min" type="u32" required> | ||
| Tokens replenished per minute per chat. | ||
| </ParamField> | ||
|
|
||
| **Example:** | ||
| ```rust | ||
| // Tighten during an observed fan-out storm | ||
| client.set_resend_rate_limit(5, 3); | ||
|
|
||
| // Restore defaults | ||
| client.set_resend_rate_limit(20, 10); | ||
|
|
||
| // Disable entirely | ||
| client.set_resend_rate_limit(0, 0); | ||
| ``` | ||
|
|
||
| <Tip> | ||
| Configure at build time via [`BotBuilder::with_resend_rate_limit`](/api/bot#with-resend-rate-limit). Use this method only to tune live without restarting. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This new cross-link points to Useful? React with 👍 / 👎.
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
|
||
| </Tip> | ||
|
|
||
| ### resends_throttled_total | ||
|
|
||
| ```rust | ||
| pub fn resends_throttled_total(&self) -> u64 | ||
| ``` | ||
|
|
||
| Returns the total number of outbound resends dropped by the per-chat rate limiter since the client started. A rising value indicates an active retry storm is being throttled. Available without the `debug-diagnostics` feature. | ||
|
|
||
| **Example:** | ||
| ```rust | ||
| let dropped = client.resends_throttled_total(); | ||
| if dropped > 0 { | ||
| tracing::warn!("Resend storm: {} resends throttled by rate limiter", dropped); | ||
| } | ||
| ``` | ||
|
|
||
| ### set_force_active_delivery_receipts | ||
|
|
||
| ```rust | ||
|
|
@@ -1825,6 +1875,8 @@ Returns a snapshot of all internal collection sizes for memory leak detection. M | |
| | `sender_key_device_cache` | `u64` | Per-group sender key device tracking cache | | ||
| | `session_locks` | `u64` | Per-device session locks | | ||
| | `chat_lanes` | `u64` | Per-chat lanes (combined enqueue lock + message queue) | | ||
| | `resend_rate_limiter_chats` | `u64` | Active per-chat resend rate-limiter token buckets | | ||
| | `resends_throttled_total` | `u64` | Total outbound resends dropped by the rate limiter since start | | ||
| | `response_waiters` | `usize` | Active IQ response waiters | | ||
| | `node_waiters` | `usize` | Active node waiters | | ||
| | `pending_retries` | `usize` | Pending message retries | | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| --- | ||
| title: "June 15, 2026 — Per-chat outbound resend rate limiter" | ||
| description: "Adds a token-bucket rate limiter on group retry resends to prevent AccountLocked during PN-to-LID migration fan-outs. Tunable via BotBuilder::with_resend_rate_limit and Client::set_resend_rate_limit." | ||
| --- | ||
|
|
||
| PR [#871](https://github.com/oxidezap/whatsapp-rust/pull/871) adds a per-chat token-bucket rate limiter on outbound retry resends to protect against the `AccountLocked` error produced by high-volume PN-to-LID migration fan-outs. | ||
|
|
||
| ## Problem | ||
|
|
||
| During a mass PN-to-LID migration, hundreds of distinct devices each fail to decrypt the same group messages and each send a retry receipt. The library pairwise-resends to every device, and WhatsApp's anti-abuse system penalizes the **aggregate per-chat resend rate** — not any single device's depth. The existing `MAX_RETRY_COUNT=5` guard trusts the peer-echoed `count` field, which stays at `1` during a fan-out, so it never fires. The result is an `AccountLocked` error under sustained fan-out conditions. | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| ## Fix | ||
|
|
||
| A per-chat token-bucket rate limiter applied to every outbound group retry resend: | ||
|
|
||
| - **Keyed by chat JID**: bounds the aggregate resend rate regardless of how many distinct devices drive it. | ||
| - **Drop, don't queue**: throttled devices are already marked for fresh SKDM earlier in the retry path — they recover on the next normal group send without any timer or queued work. The hot path stays allocation-free. | ||
| - **Lazy refill off the monotonic clock**: correct over long sessions, immune to wall-clock jumps, no background task required. | ||
| - **Capacity-only cache**: bounds memory; eviction is fail-open (an evicted bucket recreates full), so undersizing only forgives rate, never over-throttles. | ||
| - **Group-only**: DMs have no SKDM fallback, so they keep the unconditional resend (bounded by `MAX_RETRY_COUNT`) to avoid dropped deliveries. | ||
|
|
||
| ## Defaults | ||
|
|
||
| | Parameter | Default | Description | | ||
| |-----------|---------|-------------| | ||
| | Burst | 20 | Instantaneous allowance per chat | | ||
| | Refill rate | 10 / min | Sustained per-chat ceiling | | ||
|
|
||
| The defaults are conservative: well under the rate observed to trip `AccountLocked`, yet above any healthy chat's steady resend need. A burst of `0` disables the limiter entirely. | ||
|
|
||
| ## New API | ||
|
|
||
| ### `BotBuilder::with_resend_rate_limit` | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| Configure the limiter at build time: | ||
|
|
||
| ```rust | ||
| Bot::builder() | ||
| // ... | ||
| .with_resend_rate_limit(30, 15) // burst 30, refill 15/min | ||
| .build() | ||
| .await? | ||
| ``` | ||
|
|
||
| ### `Client::set_resend_rate_limit` | ||
|
|
||
| Retune live without reconnecting: | ||
|
|
||
| ```rust | ||
| // Tighten during an observed fan-out storm | ||
| client.set_resend_rate_limit(5, 3); | ||
|
|
||
| // Disable entirely | ||
| client.set_resend_rate_limit(0, 0); | ||
| ``` | ||
|
|
||
| ### `Client::resends_throttled_total` | ||
|
|
||
| Surfaces storm activity without the `debug-diagnostics` feature: | ||
|
|
||
| ```rust | ||
| let dropped = client.resends_throttled_total(); | ||
| if dropped > 0 { | ||
| tracing::warn!("Resend storm: {} resends dropped by rate limiter", dropped); | ||
| } | ||
| ``` | ||
|
|
||
| ### `CacheConfig::resend_rate_limiter_capacity` | ||
|
|
||
| Controls the per-chat bucket cache capacity (default: 4096). Keep this above the count of groups concurrently driving retry storms. Eviction is fail-open. | ||
|
|
||
| ### `MemoryDiagnostics` new fields | ||
|
|
||
| Two new fields are available in the snapshot returned by `client.memory_diagnostics()`: | ||
|
|
||
| | Field | Description | | ||
| |-------|-------------| | ||
| | `resend_rate_limiter_chats` | Active per-chat token buckets | | ||
| | `resends_throttled_total` | Total resends dropped since start | | ||
Uh oh!
There was an error while loading. Please reload this page.