Overview
WhatsApp Web has no volume-based throttle on inbound retry receipts.WAWebHandleRetryRequest serializes them per chat, refuses past MAX_RETRY, and otherwise processes every receipt — its gates are all semantic (ALREADY_DELIVERED, CHANGED_IDENTITY, RECORD_MISSING, DEVICE_NOT_RECIPIENT, HIGH_RETRY_COUNT, MESSAGE_EXPIRED), never “this member is asking too often.” This SDK mirrors that: by default, every inbound <receipt type="retry"> runs the full repair path (markForgetSenderKey, key-bundle processing, resend).
A single-user WA Web client never sends at a volume where this matters. A bot in a large group can, if a cohort of members has pairwise sessions that never establish — each of those members retries on every message, driving the repair path at storm rate.
Registering a RetryAdmission policy lets you bound that cost without the SDK diverging from WhatsApp Web by default. Leaving it unset keeps exact WA Web behavior, at zero cost — the check on the receive path is a single OnceLock::get().
This is an opt-in seam for bot-scale deployments. Dropping a retry receipt is a deliberate decision to skip an eligible repair request; the SDK will never do this on its own.
Scope
The policy is only consulted for group andstatus@broadcast retry receipts from other accounts. It is never consulted for:
- Retries from your own companion devices (
is_peer) — their session must always be able to rebuild. - Any 1:1 (DM) retry.
admit returns false, the receipt is dropped before the group-info fetch, the unknown-device rotateKey block, markForgetSenderKey, key-bundle processing, and the resend. A dropped receipt is not queued — the requester re-requests on its own timer, so a policy should refill over time to keep genuine recovery possible.
The RetryAdmission trait
MaybeSendSync is Send + Sync on native targets, unbounded on wasm32).
admit is called inline on the receive path and is deliberately synchronous — a slow or awaiting policy would stall retry processing for the pending key, and a gate never needs to wait. Keep it to a fast, local decision (an atomic counter, a token-bucket check); do no I/O or blocking inside it.
The device is intentionally part of requester, but a policy may key on the user alone: WhatsApp Web re-targets a whole user’s sender key when its primary device goes cold, so all devices of one broken account can reasonably share a single budget.
RetryAdmission is re-exported from the crate root and from the prelude.
Opting in
Client::set_retry_admission returns false (and keeps the previously-registered policy) if called more than once — set it once, before connecting. Live tuning belongs inside the policy itself (e.g. atomics), not in re-registration.
Example: token-bucket quarantine
The repository shipsexamples/retry_quarantine.rs: a per-(chat, requester) token bucket, burst 2 / refill 2 per day, with a bounded keyspace (fails open — admits — once the tracked-pair cap is reached, rather than growing unbounded or blocking a brand-new pair).
- Keyed by user, not device —
(chat.user, requester.user), matching the “one broken account, one budget” rationale above. - Burst 2 repairs a healthy member immediately (one mark is enough — the next send already carries the SKDM); receipts past the burst from the same pair are dropped before any repair work.
- Refill 2/day keeps genuine recovery possible for a member whose session is intermittently broken, without allowing a sustained storm.
burst = 0disables the policy outright (always admits), useful for a kill switch without unregistering.
Design rationale
This hook is a decoupled, WA Web-compliant alternative to embedding a quarantine directly in the SDK’s default retry-receipt path: the core stays byte-for-byte WA Web, and any volume-based policy — including the exact mechanism above — lives in operator code via this trait instead of being on by default.See also
- Inbound Durability Hook — the sibling opt-in hook idiom (
OnceLock, zero overhead unset) that this trait follows.