Skip to main content

Async by design

What this covers / who it's for. The four deliberate not-instant patterns in the platform — submit-then-listen, request-then-poll, fleet/background work, and after-the-action forwarding — what each status means to a user, how polling should behave, and how a lost session recovers. Read this before designing any UI or support flow around "why isn't it done yet".

There is no push transport in V1 — realtime is polling, by decision (ADR D67: the FE polls; broadcast/websockets are V2 — see the ADR index). Everything below describes what the client sees while polling.

Pattern 1 — Submit-then-listen (provider actions)

Anything the finance rail executes returns immediately as accepted, and the truth arrives later by webhook: KYB progression, wallet creation, payout and order status, co-sign confirmation. The user sees a pending state that resolves when the provider speaks — normally seconds, longer if webhooks stall (Providers & rails). The UI's job is to poll the record, not to wait on the request.

Pattern 2 — Request-then-poll (exports, statements, computed answers)

Long-running client requests — transaction exports and monthly statements today — return HTTP 202 plus a pollable request id instead of the result (async-requests README, payments README).

The lifecycle is fixed and small: queued → processing → succeeded | failed. Terminal states are immutable — a succeeded or failed request never changes again.

StatusWhat it means to the user
queuedAccepted; work has not started. Nothing is wrong — keep polling
processingA worker picked it up. Keep polling
succeededDone. The response carries a typed result: a link (short-lived download URL), a confirmation, or an answer (inline data)
failedTerminal, with a safe reason (never internal error detail). Retry = submit a new request

Poll etiquette (the platform's own guidance): poll every 2–5 seconds with backoff while non-terminal, stop on a terminal status. Result links are short-lived, but an expired link is never a dead end — re-fetch the request and follow the link again; the artifact download itself is separately permission-gated, so being able to see a poll result does not grant the bytes.

Recovery surface: a reload or a returning session re-discovers its in-flight and recent requests through the async-request list endpoint (filterable, newest-first) and resumes polling by id. The list is retention-bounded (terminal requests are pruned after 30 days — Data lifecycle guarantees), so it is recent history, not an archive.

Pattern 3 — Fleet & background work (the task runner)

Scheduled multi-unit work — reconciliation sweeps, monthly statement generation, document forward re-drives — runs as batched task runs: one unit per tenant (or per item), tracked on a durable, operator-visible ledger (task-runner README). Product-relevant guarantees:

  • One unit's failure never aborts the fleet — one tenant's error can't stop everyone else's statement.
  • Failed units are retryable by operators; the ledger (also exposed as a backoffice API) shows exactly which tenant/unit failed and why.
  • Stuck detection: a unit still "running" after 120 minutes is flagged by an hourly sweep — detection and alerting only, never silent auto-healing.

Pattern 4 — After-the-action forwarding (documents)

Uploading a KYB/KYC document succeeds when it lands in our vault; forwarding it to the rail happens afterwards, on a queue. A forward failure never fails the upload — failed forwards are re-driven by an hourly sweep (documents README). Consequence: "uploaded here" and "received by the provider" are two separate moments, usually seconds apart.

The product-relevant clock (what runs when)

The full schedule lives in Observability & ops; the handful a product person should know:

CadenceWhat
Every 15 secondsFX rate cache warming (rates served ≤ 20s stale)
Every minuteCompliance SLA breach scan
HourlyStale-mirror reconciliation; document forward re-drive; stuck-task detection
DailyRetention prunes; document integrity sweep
Monthly (1st, 02:00)Statement generation for every tenant

← Product wiki index