How fast should a quote API respond? Latency, timeouts, and the quote window

Measure quote API latency at p50 and p99, set timeouts that fit a 5-minute or 10-second quote window, cache rates safely, and retry without duplicates.

A quote API should answer well inside the time your user will wait, and your client timeout should come from measured tail latency, not a guess. The number that matters is not the average. It's the p99, and how much of the quote window the slowest call, a retry, and the approval step eat before you execute.

Most teams never look at this until a payout fails with an expired quote and nobody can say where the five minutes went. The math is simple once it's on a whiteboard. This page puts it there. For why the window exists at all, start with how on/off-ramp providers make money, since every locked second is risk somebody prices.

Key takeaways

  • Track latency as a distribution. The mean hides the slow calls that break short quote windows.
  • Set timeouts from a false-timeout target: 0.1% of calls timing out points to the p99.9.
  • A 5-minute quote forgives slow calls. A 10-second OTC quote doesn't.
  • Read expires_at (epoch milliseconds) from every response instead of hardcoding a TTL.
  • Cache indicative rates for seconds. Hold a binding quote only until it expires.
  • Retry with backoff, jitter, and an Idempotency-Key, resending the exact same body.

Why does quote latency matter if the quote lasts 5 minutes?

Because the window is shared. The clock starts when the provider creates the quote, and everything after that runs against it: the response traveling back, your UI rendering the amount, a human approving, and the execute call.

On BlindPay, payin and payout quotes default to 5 minutes. OTC payin quotes expire in 10 seconds, and SEPA payout quotes can be shorter than 5 minutes because the rail's own deadline can be tighter (cut-off times). That's why the docs say to read expires_at rather than assume a fixed lifetime.

A 2-second slow response is 0.7% of a 5-minute window. Rounding error. The same 2 seconds is 20% of a 10-second OTC window. Not great. Short windows exist because the provider is holding a price in a moving market; why a locked quote costs money explains that trade.

Which latency numbers should you measure?

Measure p50, p95, and p99 per endpoint, and track failed calls separately from successful ones. Averages lie about payments.

Google's SRE book makes the point with an example: a service with a 100 ms average can still have 1% of requests take 5 seconds, and when several backends feed one page, "the 99th percentile of one backend can easily become the median response of your frontend" (Google SRE book). It also warns that "a slow error is even worse than a fast error", so don't filter errors out of your latency charts.

What to record on every quote call:

  1. Request start and response end, from your own server, in milliseconds.
  2. The HTTP status and error code, if any.
  3. The quote ID and expires_at.
  4. The time left on the quote when you called execute.

The fourth one is the metric people skip. It tells you directly whether latency plus human time is eating the window. Bucket all of it into histograms, the format the SRE book recommends, so the tail is visible.

How do you set a client timeout for a quote request?

Start from a target false-timeout rate, then read the percentile. AWS describes the method for its own services: "we choose an acceptable rate of false timeouts (such as 0.1%). Then, we look at the corresponding latency percentile on the downstream service (p99.9 in this example)" (AWS Builders' Library).

Then cap it with a budget tied to the window. A timeout is only useful if a retry can still finish inside the quote's life.

CallWhat it doesTimeout guidanceRetry?
Indicative rate (/payin-quotes/fx)Preview price, reserves nothingShort; drop stale previewsYes, freely
Payin or payout quoteLocks rate and fees for the windowNear your measured p99.9Yes, same body
OTC payin quoteLocks a BRL price for 10 secondsSmall slice of 10 secondsOnly if time remains
Execute payout or payinMoves moneyNear p99.9, plus marginOnly with Idempotency-Key

If your measured p99.9 for a quote call is longer than you can afford on a short window, the fix is not a shorter timeout. It's a flow change: request the quote at the moment of confirmation, not when the page loads.

Should you cache quotes or rates?

Cache rates, not commitments. The two calls behave differently.

An indicative rate reserves nothing. BlindPay's POST /payin-quotes/fx returns a rate and amount "without creating a payin quote", and the docs say plainly that its numbers "aren't guaranteed to match the quote you get" a moment later (payin quotes). Caching that for a few seconds behind a typing field is fine. Indicative rate vs binding quote covers the two-call pattern.

A binding quote is different. You can hold its ID until expires_at to execute it once. You can't reuse it for another amount, another recipient, or after expiry. Two mistakes show up often:

  • Caching a quote on page load. The user reads, gets coffee, and clicks pay on a dead quote.
  • Showing a cached amount after re-quoting. The screen says one number, the payout settles another.

Rule of thumb: the number on the confirm button must come from a quote whose expires_at is still in the future when the click lands.

How do retries work without creating duplicates?

With idempotency keys, backoff, and jitter. Retries are selfish: AWS notes that "when a client retries, it spends more of the server's time to get a higher chance of success", and that retries "can make the overload worse" (AWS Builders' Library). So space them out with exponential backoff and add jitter.

The money side needs more than spacing. On BlindPay, any authenticated POST, PUT, PATCH, or DELETE under /v1/* accepts an Idempotency-Key header of up to 255 characters (idempotency):

  • A retry replays only if the body matches byte for byte. The check is a SHA-256 of the raw body.
  • Same key, different body returns 422 idempotency_key_payload_mismatch.
  • Same key while the first call is still running returns 409 idempotency_key_in_flight with a Retry-After header.
  • A 5xx releases the key, so the retry runs fresh.
  • Keys are kept for 24 hours.

Use one key per attempt to execute a specific quote. A new quote gets a new key. Idempotency keys for stablecoin APIs walks through the failure modes in depth.

Illustrative example: how much of the window does latency eat?

Illustrative example. A payout flow with one human approval, measured on a team's own servers. The numbers are made up for the walkthrough, not BlindPay figures.

StepTime usedTime left on a 5-minute quoteTime left on a 10-second quote
Quote request (p99)1.5 s298.5 s8.5 s
Render amount to approver0.5 s298.0 s8.0 s
Human approval45 s253.0 sExpired
Execute call (p99)1.5 s251.5 sn/a

The 5-minute flow finishes with four minutes to spare even on the slow path. The 10-second flow can't fit a human at all. That's the real lesson for OTC: automate the confirmation, or have the approver approve the amount range before the quote is requested.

Now add one timed-out quote request with a 3-second timeout and a retry. On the 5-minute quote, nothing changes. On the 10-second quote, the retry alone leaves about 5 seconds for everything else.

How do you keep latency from breaking payouts in production?

Three habits cover most of it.

  1. Alert on the tail, not the mean. Page someone when the p95 of quote calls doubles against last week, before expired quotes show up in support.
  2. Track time left at execute. If the median drops under a minute on 5-minute quotes, your approval step is the bottleneck, not the API.
  3. Re-quote on expiry, never extend. An expired quote is rejected. Request a fresh one, show the new amount, and ask again.

Teams comparing providers should run the same harness against each one from the same region, at the same hours, for a week. Latency differences between providers are often smaller than the differences between your own regions.

What to do next

Instrument one quote call today: log start, end, status, expires_at, and the time left when you executed. A week of that data tells you your real timeout and whether your approval flow fits the window. The payout quotes reference has every field you need to log.

FAQ