Back to News & Insights
Web Development October 10, 2026 · 10 min read

Who gave your frontend permission to retry?

Almost every frontend codebase has this somewhere, usually in a file called api.ts: async function...

Who gave your frontend permission to retry?

Almost every frontend codebase has this somewhere, usually in a file called api.ts:

It reads as resilience. From the server's side it is something else: a rule that says whenever the backend is struggling, every open tab sends it four times the traffic, immediately, all at once.

A retry is a request the server did not ask for. Backend engineers have treated that as a design problem for years, with backoff, jitter, budgets and circuit breakers. Frontend code mostly has not, and the frontend is the layer where it matters most. A backend service has a known number of callers. A web app has as many callers as there are open tabs, none of them coordinate, and all of them noticed the outage in the same second.

This post is about what a respectful retry looks like from the browser: backoff, jitter, and the four things the browser adds that backend retry guides never mention.

The first fix everyone reaches for is exponential backoff: wait 1s, then 2s, then 4s. It feels like it should spread the load. It does not, because every client computes the same delays from the same starting moment.

To put numbers on it, here is a small simulation. One thousand clients all fail at t=0, the server stays down, and each client retries four times. Delays use a 1 second base and a 30 second cap. The script counts how many retries land in each 100ms window.

Read the third row carefully. Plain exponential backoff has exactly the same peak as a fixed one second delay: all 1,000 clients arrive in the same 100ms window, four separate times. Backoff made the waves further apart. It did not make any wave smaller. A server that fell over under 1,000 simultaneous requests gets 1,000 simultaneous requests at 1s, at 3s, at 7s and at 15s, which is a good way to knock it down again each time it gets up.

The total work is identical in every row: 4,000 retries. The only thing that changes is whether they arrive as four walls or as a drizzle.

Jitter means adding randomness to the delay so clients stop moving in lockstep. The reference for this is Marc Brooker's 2015 post on the AWS Architecture Blog, Exponential Backoff And Jitter, which compared the variants and concluded that jittered backoff "should be considered a standard approach for remote clients".

Full jitter is the simplest version. Compute the exponential ceiling, then pick a uniformly random delay between zero and that ceiling:

In the simulation that one Math.random() drops the peak from 1,000 requests per window to 164, and spreads the same 4,000 retries across 137 windows instead of 4. It also finishes slightly sooner, because the average delay is half the ceiling.

Equal jitter keeps half the backoff as a guaranteed floor and randomises the other half. It is the intuitive compromise, and it did worse here (208 vs 164), which matches Brooker's own verdict that equal jitter "is the loser". The floor it protects is exactly the part that keeps clients bunched together.

The uncomfortable implication for frontend code: a delay of nearly zero is a valid outcome of full jitter. That is fine. The goal was never for each client to wait a long time. The goal is for clients to disagree with each other.

Everything above applies to any client. The next four points are specific to code running in a tab, and they are where frontend retry logic usually goes wrong. The server may have told you when to come back, and you could not read it

A 429 or 503 response can carry a Retry-After header, either as a number of seconds or as an HTTP date. If the server is telling you when it will be ready, guessing with your own backoff curve is rude and less accurate.

The browser trap is CORS. Retry-After is not on the list of response headers that cross-origin JavaScript is allowed to read by default. If your API lives on a different origin from your app, res.headers.get('retry-after') returns null even though the header is plainly visible in the network tab. The server has to opt in:

Without that line, the backend team can implement rate limiting perfectly and the frontend will ignore it, silently. Libraries do not save you here. axios-retry, for example, does read retry-after and uses it as a minimum delay, but it reads from the same header object the browser already filtered.

One more detail: when a rate limiter sends Retry-After: 30 to a thousand clients, it has just synchronised them. Honour the header as a floor and add your jitter on top of it, not instead of it. A hidden tab should not be retrying at all

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation