diff --git a/advanced/signal-protocol.mdx b/advanced/signal-protocol.mdx
index 940ede7..82d7a42 100644
--- a/advanced/signal-protocol.mdx
+++ b/advanced/signal-protocol.mdx
@@ -1133,6 +1133,19 @@ impl SessionStore for SessionAdapter {
}
```
+### Flush scheduling: send vs. receive
+
+*When* the dirty Signal cache reaches the backend differs by direction, because the two directions have different recovery properties:
+
+- **Send** (DM, group, and status sends) flushes **synchronously, before the stanza reaches the wire**, and propagates a persistence failure by aborting the send. Reusing an outbound counter reuses its message key and IV, so the ratchet advance must be durable before anyone can act on the ciphertext — the send must not transmit an advance it couldn't save.
+- **Receive** (live traffic, outside the offline-drain batcher) routes through a single-flight coalescing scheduler (`src/signal_flush.rs`) instead of flushing per stanza: a burst of receives folds into one flush per ~25ms window, retried with exponential backoff (up to a 5s cap) on backend failure. This is safe because a lost receive-side advance simply re-derives forward on the next message (the receiving chain derives `CK_n → CK_n+1`), and a consumed one-time prekey stays buffered until its session is durable — a crash inside the window is recoverable.
+
+The scheduler is generation-scoped (embeds the connection generation in its atomic state), so a reconnect during an in-flight flush needs no explicit reset: a stale worker from the previous connection cannot mutate the new generation's state, and stands down when it observes a foreign generation.
+
+The offline drain, retry-receipt recovery, identity-change recovery, and teardown all keep their own **synchronous** flushes — they gate acks, receipts, or follow-up reads on durability and are not routed through the receive coalescer. See [Inbound Durability Hook](/advanced/inbound-durability) for the drain-batch commit ordering, which this coalescing does not change.
+
+Call [`Client::flush_pending_signal_state()`](/api/client#flush_pending_signal_state) to force a deterministic settle — e.g. before reading persisted Signal state directly, or ahead of a non-graceful shutdown. Never call it from inside an `InboundDurabilityHook` or a synchronous, inline `EventHandler::handle_event` implementation, since settling re-enters the processing permit those run under and would deadlock during an offline-sync drain. Ordinary `Bot` closure handlers are unaffected — both default delivery modes run the callback in a detached task off the permit.
+
## Security Considerations
### Identity key trust
diff --git a/api/client.mdx b/api/client.mdx
index 014b9ed..6ec71d0 100644
--- a/api/client.mdx
+++ b/api/client.mdx
@@ -1455,6 +1455,29 @@ Send pre-marshaled plaintext bytes through the noise socket. The bytes must be a
This bypasses node logging and `wait_for_sent_node` waiter resolution. Use [`send_node`](#send_node) for normal stanza sending. This method is intended for performance-critical paths where you already have marshaled bytes.
+### flush_pending_signal_state
+
+```rust
+pub async fn flush_pending_signal_state(&self) -> Result<(), anyhow::Error>
+```
+
+Forces any pending write-behind Signal cache state to the backend, returning once the flush completes (or fails).
+
+Ordinarily — without calling this method — the backend trails the in-memory cache: outbound sends already flush synchronously, but the live receive path only schedules a coalesced flush every ~25ms window (see [flush scheduling](/advanced/signal-protocol#flush-scheduling-send-vs-receive)). A successful call to `flush_pending_signal_state()` closes that gap deterministically: everything dirty as of the call is persisted by the time it returns `Ok`. The call itself has **no hard wall-clock bound**, though — it can wait on locks or on slow/failing storage (a backend outage extends it until the retry loop succeeds). Check the returned `Result`: a failure means the flush did not complete and state is still pending, not persisted.
+
+
+Never call this from inside an [`InboundDurabilityHook`](/advanced/inbound-durability) — during an offline-sync drain it runs while the processing permit is held, and settling routes through that same permit, so re-entering it would deadlock. The same risk applies to a custom `EventHandler::handle_event` implementation that itself blocks synchronously inline (dispatch is synchronous). It does **not** apply to ordinary [`Bot`](/api/bot) closure handlers (`.on_message()`, etc.) — both the default concurrent and ordered delivery modes run your callback in a detached task that never holds the permit, so calling `flush_pending_signal_state()` from inside one of those is safe.
+
+
+**Example:**
+```rust
+// Force durability before reading Signal state directly, or before a
+// non-graceful shutdown (process kill, container stop).
+client.flush_pending_signal_state().await?;
+```
+
+See [Signal Protocol — flush scheduling](/advanced/signal-protocol#flush-scheduling-send-vs-receive) for the full durability model.
+
### generate_message_id
```rust
@@ -2001,8 +2024,6 @@ if let Some(alloc) = report.alloc {
Storage, transport, and HTTP reports are supplied by the trait implementations behind `Client` — see [`DeviceStore::resource_report`](/api/store#resource_report), [`Transport::resource_report`](/api/transport#resource_report), and [`HttpClient::resource_report`](/api/http-client#resource_report). `AllocSnapshot`, `StorageResourceReport`, `TransportResourceReport`, and `HttpResourceReport` are re-exported from `wacore::stats`; all four are also re-exported from the `whatsapp_rust` crate root.
----
-
## Error Types
```rust
diff --git a/api/send.mdx b/api/send.mdx
index eddcc63..9a6fd67 100644
--- a/api/send.mdx
+++ b/api/send.mdx
@@ -43,6 +43,10 @@ pub async fn send_message(
Contains the `message_id` (unique ID for tracking receipts, edits, revokes) and `to` (resolved recipient JID). Use `send_result.message_key()` to get a `wa::MessageKey` for album child linking, pinning, or other operations that reference this message.
+
+For DMs, group, and status sends, the outbound Signal ratchet advance is persisted to the backend **synchronously, before the stanza is transmitted** — reusing an outbound counter would reuse its message key and IV, so the advance must be durable before anyone can act on the ciphertext. If that persistence write fails, `send_message` returns `Err` instead of transmitting an advance that couldn't be saved. See [Signal Protocol — flush scheduling](/advanced/signal-protocol#flush-scheduling-send-vs-receive) for the full durability model.
+
+
### SendResult
Result of a successfully sent message. Provides the message ID and a convenience method to construct a `MessageKey` for follow-up operations like album child linking.
@@ -1252,7 +1256,7 @@ pub enum SendError {
- `Iq` — IQ request required by the send path failed
- `InvalidRequest` — the send request was malformed (e.g., invalid JID, bad message shape)
- `Client` — underlying transport/connection error
-- `Internal` — catch-all for errors not yet assigned a typed variant
+- `Internal` — catch-all for errors not yet assigned a typed variant. Includes a failure to durably persist the outbound Signal ratchet advance before the stanza was sent — see the durability note under [`send_message`](#send_message).
**Example:**