Skip to content

Receive: history is written one HTTP call per token, serialized inside the drain — 11.9 s for 54 tokens #759

Description

@MastaP

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 arraypostHistory(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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions