Skip to content
4 changes: 2 additions & 2 deletions advanced/websocket-handling.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1047,7 +1047,7 @@ Each stream error sets `is_logged_in = false` and fires the `shutdown_notifier`
|------|--------|-------|-------------|
| **401** | Disables auto-reconnect | `LoggedOut` | No — session invalid, must re-pair |
| **409** | Disables auto-reconnect | `StreamReplaced` | No — prevents displacement loop |
| **429** | Adds 5 to backoff counter | None | Yes — extended Fibonacci backoff |
| **429** | Adds 5 to backoff counter | `StreamError` | Yes — extended Fibonacci backoff |
| **503** | Normal handling | None | Yes — standard backoff |
| **515** | Marks as expected disconnect | None | Yes — immediate, no backoff |
| **516** | Disables auto-reconnect | `LoggedOut` | No — device removed |
Expand Down Expand Up @@ -1119,7 +1119,7 @@ Maximum: 900s (15 minutes)
Jitter: ±10%
```

For rate-limited errors (429), the backoff counter is incremented by 5 before the normal increment, causing the delay to jump significantly on the next reconnection attempt.
For rate-limited errors (429), the client increments the backoff counter by 5 before the normal increment, causing the delay to jump significantly on the next reconnection attempt. Since [#1263](https://github.com/oxidezap/whatsapp-rust/pull/1263), the client also dispatches `Event::StreamError` for 429 — WA Web gives its UI no signal here at all (429 is outside the `500..600` range its handler special-cases), but an embedder has no UI to fall back on, so the rate limit is reported like every other coded stream error.

**Stability-gated reset.** The backoff counter does not reset to its base immediately on a successful `<success>` authentication. Instead, `connected_at_ms` records the auth time, and the counter only resets when the *next* disconnect finds the connection was stable for at least `STABLE_CONNECTION_RESET_MS` (30s) — matching WA Web's `resetDelay`. A connection that authenticates and then immediately drops keeps escalating the backoff instead of resetting to 1s and retrying in a tight loop.

Expand Down
4 changes: 3 additions & 1 deletion api/client.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ The client handles specific `<stream:error>` codes from the WhatsApp server:
|-------------------|---------|---------------|----------------|
| **401** | Session invalidated (unauthorized) | `LoggedOut` | Disabled — must re-pair |
| **409** | Another client connected (conflict) | `StreamReplaced` | Disabled — prevents displacement loop |
| **429** | Rate limited (too many connections) | None (emits `Disconnected`) | Yes, with extended backoff (+5 steps) |
| **429** | Rate limited (too many connections) | `StreamError`, then `Disconnected` | Yes, with extended backoff (+5 steps) |
| **503** | Service unavailable | None | Yes, normal backoff |
| **515** | Expected disconnect (e.g., post-pairing) | None | Yes, immediate (no backoff) |
| **516** | Device removed | `LoggedOut` | Disabled — must re-pair |
Expand All @@ -509,6 +509,8 @@ When you receive a `LoggedOut` or `StreamReplaced` event, auto-reconnect is perm

When the server returns a 429 stream error, the client bumps the internal backoff counter by 5 Fibonacci steps before reconnecting. This means the reconnection delay jumps significantly (e.g., from ~1s to ~13s on the first rate limit) to respect the server's throttling.

As of [#1263](https://github.com/oxidezap/whatsapp-rust/pull/1263), the client also dispatches an `Event::StreamError` (code `"429"`) — WhatsApp Web itself gives no signal for this case (its handler only special-cases `500..600`), but an embedder has no UI to fall back on, so a rate-limited session is now observable the same way every other coded stream error is. This is in addition to, not instead of, the `Event::Disconnected` the shared connection-loss path already dispatches once the socket actually closes — the 429 handler never marks the disconnect as expected, so it takes the same route every other unexpected drop does.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Split the rate-limit explanation into concise sentences

The added paragraph packs change history, WhatsApp Web behavior, embedder rationale, event ordering, and reconnect semantics into two long sentences. Split these into single-idea sentences so the event contract remains easy to scan and complies with the repository's explicit concise-sentence standard.

AGENTS.md reference: AGENTS.md:L25-L25

Useful? React with 👍 / 👎.


### General reconnection behavior

| Scenario | Behavior |
Expand Down
6 changes: 3 additions & 3 deletions concepts/events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ Event::LoggedOut(logout) => {

### StreamError

**Emitted:** For unrecognized stream error codes (codes not matching 401, 409, 429, 503, 515, or 516)
**Emitted:** For a `429` (rate-limited) stream error, and for unrecognized stream error codes (codes not matching 401, 409, 503, 515, or 516). Since [#1263](https://github.com/oxidezap/whatsapp-rust/pull/1263), the client reports 429 the same way as every other coded branch — WhatsApp Web's own handler has no arm for it either (only `500..600` is special-cased there), so this is an embedder-facing choice rather than a fidelity fix.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep 429 out of the unrecognized-code definition

The parenthetical now defines an unrecognized code as any code not matching 401, 409, 503, 515, or 516, which makes 429 satisfy that definition even though this sentence and the list below treat it as a specific rate-limit case. Retain 429 in the exclusion list while separately noting that it emits the generic StreamError event.

Useful? React with 👍 / 👎.


```rust
#[derive(Debug, Clone, Serialize, bon::Builder)]
Expand All @@ -482,10 +482,10 @@ Event::StreamError(err) => {
```

<Note>
Recognized stream error codes emit specific events instead of `StreamError`:
Specific stream error codes have the following event behavior:
- **401** → `LoggedOut` (session invalidated)
- **409** → `StreamReplaced` (another client connected)
- **429** → No event emitted (reconnects with extended backoff)
- **429** → `StreamError` (rate limited; also reconnects with extended backoff)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the example that labels every StreamError unknown

Once 429 reaches this event, the unchanged usage example above prints Unknown stream error for a known rate-limit condition. Consumers who copy the example will therefore misclassify 429s in their logs; use a neutral label or branch on err.code before describing the event as unknown.

Useful? React with 👍 / 👎.

- **503** → No event emitted (reconnects with normal backoff)
- **515** → No event emitted (immediate reconnect, e.g., after pairing)
- **516** → `LoggedOut` (device removed)
Expand Down