API retry logic for an irreversible purchase starts with classification, not with a loop counter. Retry only errors you know are transient, never retry a definitive rejection, and treat a timeout as an unknown outcome that must be resolved before any second attempt goes out.
Gift card and top-up orders make the stakes concrete: the thing a careless retry buys twice cannot be returned. One mechanism underneath is non-negotiable and has its own article: every retry must carry the same idempotency key as the first attempt. This piece is about the policy layer on top: when to retry at all, how fast, and when to stop.
Classify first, retry second
Every failed request falls into one of three classes, and the class decides everything.
A definitive rejection means the provider received the request, understood it, and said no: invalid product ID, insufficient balance, a blocked account. Retrying changes nothing except your logs. Fix the cause, then place a new order on purpose. The taxonomy of these responses is in the order failure field guide.
A transient error means the request never took effect: a 429 rate limit, a 502 or 503 from a gateway, a connection refused before anything was sent. These are the only errors that earn an automatic retry.
A timeout is neither. The request may have completed after your client stopped waiting, so treating it as a failure is a guess. Query the order’s status by your reference first, and re-send only when the provider confirms the order never registered, always under the original key.
| Error class | Retry? | With what |
|---|---|---|
| Definitive rejection (invalid product, insufficient balance) | Never | Fix the cause, then place a fresh order deliberately |
| Transient error (429, 502, 503, connection refused) | Yes | Bounded backoff with jitter, same idempotency key |
| Timeout (no response received) | Not blindly | Status query first; re-send only under the original key |
Backoff, jitter, and a hard budget
Bounded exponential backoff is the standard shape: wait a second or two before the first retry, then double the gap each time. The bound matters as much as the curve. Three to five attempts inside a few minutes is a sensible budget for a purchase. A purchase that finally succeeds an hour after checkout answers a question nobody is asking any more: the customer has left, and the quoted price may have moved.
Jitter, a random offset added to each gap, gets skipped often and matters more than it looks. Without it, a hundred workers that failed together retry together, in synchronised waves that hit the recovering provider like a metronome. With it, the same load spreads into harmless noise.
When the budget is spent
Spending the budget does not mean the order failed. Park it in a distinct state, unresolved, that is neither complete nor cancelled. Then do three things: query the provider’s status endpoint by your reference, surface the order to a human queue with its full attempt history, and let daily reconciliation sweep up anything that slips past both. An unresolved order that quietly becomes “failed” in your database while the provider fulfilled it is how phantom stock losses are born.
Stop hammering a failing provider
Retries assume the problem is local and brief. During a provider incident it is neither, and a circuit breaker belongs in the client: after a run of consecutive transient failures, stop sending entirely for a cooling-off period and probe with single requests before reopening.
Resist the tempting alternative of queueing new orders blind while the circuit is open. A queue drained at recovery fires hundreds of stale purchases at once, at prices quoted before the incident, into an API still finding its feet. Pausing sales for twenty minutes is a better outcome than explaining that surge later.
Retries cluster on the worst days
The uncomfortable property of retry logic is that it does nothing on good days and multiplies traffic on bad ones. Errors spike exactly when infrastructure degrades, so every client starts retrying at the same time, deepening the degradation that caused the errors. Ambiguous outcomes multiply too: a slow API produces more timeouts, which means more unknown states to resolve at the moment your team is busiest. The budget, the jitter and the breaker all exist for this one day.
Frequently asked questions
When should an API request be retried?
Retry only when the error is known to be transient: rate limiting, gateway errors such as 502 or 503, or a connection that failed before the request was sent. Never retry a definitive rejection like an invalid product or insufficient balance, since the answer will not change. Treat timeouts separately: the outcome is unknown, so query the order’s status before deciding to re-send.
What is exponential backoff with jitter?
Exponential backoff spaces retries with growing gaps, for example one second, then two, then four, so a struggling service gets room to recover. Jitter adds a random offset to each gap so that many clients which failed at the same moment do not retry in synchronised waves. Combined with a hard cap on attempts, this is the standard retry timing pattern for production APIs.
How many times should I retry a payment-like API call?
Three to five attempts within a few minutes is a reasonable budget for purchases and similar irreversible calls. Beyond that, the context that justified the order has gone stale: the customer may have left and prices may have changed. When the budget is spent, mark the order unresolved, query the provider for its status, and route it to a human rather than continuing to retry.
Should a timed-out purchase request be retried?
Not blindly. A timeout means the outcome is unknown: the provider may have fulfilled the order after your client stopped waiting, so an immediate re-send risks buying twice. Query the order’s status by your own reference first. If the provider confirms nothing registered, re-send under the original idempotency key; if the order exists, deliver it and skip the retry entirely.
Why use a circuit breaker with purchase API retries?
Because retries assume the fault is local and brief, and during a provider incident it is neither. A circuit breaker watches for consecutive transient failures, then stops sending for a cooling-off period and probes with single requests before reopening. That protects the recovering API from synchronised retry waves, and it is safer than queueing orders blind, since a drained queue fires stale purchases at pre-incident prices all at once.