Measured on staging, 2026-08-25, one 54-token receive.
The measurement
POST /v1/history x55 first 13:14:07.609 last 13:14:19.559
span 11.9 s mean gap 221 ms
For comparison, in the same receive the server-side deposit (POST /v1/mailbox/batch) took 7.0 s. The history writes cost more than the §8.2 verification of every token, and unlike the deposit they are pure round-trip latency — the server answers each one in well under 100 ms.
Why
History.post() sends exactly one record per HTTP call:
// modules/payments-v2/history/History.ts
private async post(build: () => HistoryWireRecord): Promise<void> {
const record = build();
await this.deps.client.postHistory([record]); // one record, one round trip
and the receive drain awaits it per entry, inside the loop:
// modules/payments-v2/receive/Receive.ts
const transfer = await announce(deps, entry, screened.record); // -> recordReceived -> post
So a 54-token receive serializes 55 round trips into the drain, each one blocking the next entry.
The client method already takes an array — postHistory(records: HistoryRecordWire[]) — and the §16 endpoint accepts up to 1000 records per call. Nothing on the wire needed to change; the batching seam was simply never used.
Shape of the fix
Buffer the records and post them in one call, mirroring what ackBatch (#757) did for settles:
recordReceived enqueues rather than posting, so the drain loop stops paying a round trip per entry
- flush on a size bound and once at the end of the drain, so nothing is left unwritten
history:updated is emitted per record after the batch succeeds, preserving the existing read-through mapping and event-per-entry contract for consumers
- a history failure stays non-fatal (§5.9) — it never fails the money path, which is what makes deferring it safe
Ordering note: history is written after the claim ack is queued, so it sits outside the §5.7 store-before-ack constraint. Deferring it does not touch that ordering.
Expected
55 round trips → 1. On this measurement that is most of 11.9 s off the receive. To be verified the same way it was found — against staging with the request log, not a micro-benchmark.
Refs #755, #756, #757.
Measured on staging, 2026-08-25, one 54-token receive.
The measurement
For comparison, in the same receive the server-side deposit (
POST /v1/mailbox/batch) took 7.0 s. The history writes cost more than the §8.2 verification of every token, and unlike the deposit they are pure round-trip latency — the server answers each one in well under 100 ms.Why
History.post()sends exactly one record per HTTP call:and the receive drain awaits it per entry, inside the loop:
So a 54-token receive serializes 55 round trips into the drain, each one blocking the next entry.
The client method already takes an array —
postHistory(records: HistoryRecordWire[])— and the §16 endpoint accepts up to 1000 records per call. Nothing on the wire needed to change; the batching seam was simply never used.Shape of the fix
Buffer the records and post them in one call, mirroring what
ackBatch(#757) did for settles:recordReceivedenqueues rather than posting, so the drain loop stops paying a round trip per entryhistory:updatedis emitted per record after the batch succeeds, preserving the existing read-through mapping and event-per-entry contract for consumersOrdering note: history is written after the claim ack is queued, so it sits outside the §5.7 store-before-ack constraint. Deferring it does not touch that ordering.
Expected
55 round trips → 1. On this measurement that is most of 11.9 s off the receive. To be verified the same way it was found — against staging with the request log, not a micro-benchmark.
Refs #755, #756, #757.