Engineering Diaries · Frontend Reliability
§
Frontend ReliabilityWeb PerformanceHTTP

Every Browser Retry You Add Is A Small DDoS Attack. Stop Calling It Reliability.

Retries keep a single request alive, but at production scale they can turn an outage into a traffic multiplier. The browser needs a retry policy, not a setTimeout loop.

01 · The innocent loop

The outage got worse because the UI was trying to help

One of the most dangerous lines in a frontend codebase looks completely reasonable: catch (() => setTimeout(load, 1000)). It appears after a flaky API call, usually while someone is trying to make a dashboard feel less brittle. Then a real incident arrives. The API slows down, requests start timing out, and every open tab begins asking again at the same one-second boundary.

The original failure might have been a database connection pool or a bad deploy. The browser fleet turns it into a queueing problem. Ten thousand users, three retries each, and a recovery window is suddenly receiving a second wave of work from the exact clients it failed to serve.

A retry is not a local UI detail. It is a decision to create more load during failure.

I now read retry code as infrastructure code. It needs the same questions as a message queue consumer: what is safe to repeat, how many times, how quickly, and what tells us to stop? If those questions are absent, the retry may improve a demo while making production less reliable.

02 · The multiplier

Retries can manufacture traffic precisely when capacity is lowest

Imagine a product with 20,000 active browser sessions. A failing endpoint normally receives one request per user every minute. During an incident, a naïve client retries after one second, then two, then four. Even before a user refreshes, those clients can create tens of thousands of extra requests in the first few seconds.

20k
active tabs
3
automatic retries
60k
extra attempts
0s
helpful recovery time

This is the retry storm: an overloaded service gets more work, spends more time rejecting it, and recovers later. It is not theoretical. AWS documents exponential backoff with jitter because synchronised retries from many clients create bursts. HTTP has also had a server-to-client escape hatch for years: Retry-After.

The frontend cannot repair an unavailable service. Its job is to avoid making the failure wider, give the person a clear state, and resume sensibly when the system has a chance to be healthy again.

03 · What is repeatable

Do not retry a request until you know what repeating means

Network failure is ambiguous. A browser can lose the response after the server has already processed the request. Retrying a read is usually harmless. Retrying POST /payments or POST /orders may create a second charge or a second order.

HTTP method names are useful here, but they are not a magic shield. GET, HEAD, PUT and DELETE are defined as idempotent: repeating the intended operation should reach the same state. A POST is not generally idempotent. Your own API can make a write safe to retry with an idempotency key, but that is a server contract, not something the UI can invent after a timeout.

Rule of thumb

Retry safe reads for clearly transient failures. Retry writes only when the API explicitly supports idempotency, and retain the same key across attempts.

Also distinguish a transport failure from an answer. A 400 will not become valid after 500 milliseconds. A 401 needs an authentication path. A 429 is the server asking you to slow down. Treating every non-200 as "try again" is how a client turns useful feedback into noise.

04 · Backoff

Exponential backoff without jitter still makes a queue at the door

Exponential backoff is the basic shape: wait longer after each failure, often 1, 2, 4 and 8 seconds. It reduces the number of attempts, but it has a trap. If thousands of clients fail at the same time and use the same schedule, they wake up together. You have replaced a constant flood with a series of coordinated floods.

Jitter deliberately randomises the delay. Full jitter is easy to reason about: pick a random wait between zero and the current backoff cap. The average delay is lower than the cap, but the clients are spread out instead of piled onto the same second.

retry.ts
function delayFor(attempt: number) {
const cap = Math.min(30_000, 1_000 * 2 ** attempt);
return Math.floor(Math.random() * cap);
}

That randomness is not sloppiness. It is load-shedding. Use a cap, a small retry budget, and a total deadline. A page that quietly retries forever is not resilient; it is hiding a broken contract from the person using it.

05 · The server speaks

Respect Retry-After and stop guessing what the server needs

When an API sends 429 Too Many Requests or 503 Service Unavailable with a Retry-After header, it is giving the client a direct instruction. The value can be a number of seconds or an HTTP date. Ignoring it and applying a generic one-second retry is the browser equivalent of pressing a lift button repeatedly after the sign says out of service.

There is an important nuance: honouring the header does not mean holding the UI hostage. Store the next permissible attempt, show a useful status, and let the person choose a different path if one exists. For a background refresh, simply wait. For a save action, preserve the unsaved work and be explicit that the save has not been confirmed.

One response, one policyA retry policy should use the response class, headers and operation type. A single global "three retries" helper cannot know enough.

This is also where observability matters. Log the response status, retry count, chosen delay and endpoint family. If you only measure final failures, retry code can conceal a slow degradation until the service is already busy handling its own rescue traffic.

06 · Cancellation

A retry that survives navigation is work nobody asked for

Browsers have a better primitive than a detached timer: AbortController. Use one controller for the user intent, pass its signal to fetch, and check it before a scheduled retry. When a person navigates away, changes filters, closes a panel or starts a newer search, the old operation should stop.

This is not only about bandwidth. Late responses are a correctness bug. A slow search for "react" that resolves after a faster search for "retry" can overwrite the newer result unless the request has a cancellation or versioning rule.

loadProfile.ts
const controller = new AbortController();

const response = await fetch(url, {
signal: controller.signal,
});

// Call controller.abort() when this intent is no longer current.

Do not retry an AbortError. It is usually success in another form: the product correctly decided that this work no longer matters. Treating it as failure is how cancelled requests return as ghosts.

07 · A policy

The boring retry policy I want every frontend to have

Good retry behaviour is intentionally dull. It has a small allow-list, a bounded budget and visible ownership. The individual feature decides whether a retry is worthwhile; a shared helper supplies consistent mechanics.

  • Retry only transient network errors, selected 5xx responses and carefully handled 429 responses.
  • Do not retry validation, permissions or other durable client errors.
  • Use capped exponential backoff with jitter, then stop after a short attempt or time budget.
  • Respect Retry-After when present, unless a stricter product deadline applies.
  • Bind attempts to an AbortSignal, and cancel obsolete work.
  • Require an idempotency contract before retrying a write.

For mutations, I also prefer a clear state machine over a spinner that never tells the truth: saving, saved, not saved - retry. Optimistic UI is fine when the rollback path is real. It is not a licence to pretend an unconfirmed write succeeded.

The goal is not to make failure invisible. The goal is to make failure bounded, truthful and recoverable.

08 · Verdict

Reliability starts by being a better neighbour during failure

Frontend engineers often inherit retries as a tiny utility, then forget that it runs in every tab. At that scale, each "helpful" retry is part of the system's incident behaviour. It affects server recovery, duplicate writes, battery life, analytics and whether a person trusts the screen in front of them.

The best retry policy is not aggressive. It knows when a request is safe to repeat, listens when the server asks for space, spreads unavoidable load with jitter and gives up with a useful state. That feels less magical than a generic retry wrapper. It is also how you avoid turning a partial outage into a self-inflicted stampede.

If a request has no answer after its retry budget, show the person what happened and offer a deliberate retry. A button is a better traffic policy than an immortal timer.
Written by Divyansh Singh. Views expressed are the author's own. Published October 2026. All code examples have been tested and verified.