webdevpuneet.com

Auto-Retry Loader with Exponential Backoff Countdown and Manual Retry

Auto-Retry Loader with Countdown and Manual Retry · Loaders · Plain HTML, CSS & JS · Live preview

CategoryLoaders

Copy & paste

Source Code

<div class="demo">
  <div class="retry-card" id="retryCard">
    <div class="retry-spinner" id="retrySpinner"></div>
    <p class="retry-title" id="retryTitle">Loading dashboard…</p>
    <p class="retry-sub" id="retrySub">Fetching your latest data</p>
    <button class="retry-btn" id="retryBtn" hidden>Retry now</button>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Real exponential backoff (delay doubles with each failed attempt), not a fixed or arbitrary retry interval
Live countdown timer is driven by the exact same computed delay actually being waited on, not a separate decorative animation
Manual "Retry now" button available at any point during an automatic countdown, bypassing the remaining wait
Bounded maximum attempt count with a distinct, clearly communicated give-up state rather than retrying forever
Clicking retry after giving up explicitly resets the backoff sequence, treated as a fresh attempt rather than a continuation
Visually distinct states for loading, retry-countdown, success, and given-up, each unambiguous at a glance
Structured so the simulated network call is the only piece that needs replacing for a real implementation

About this UI Snippet

Auto-Retry Loader — Exponential Backoff with an Honest, Visible Countdown

Screenshot of the Auto-Retry Loader with Countdown and Manual Retry snippet rendered live

A loading spinner that just fails silently — or one that retries immediately and repeatedly on a fixed interval — are both worse than what users actually need: clear feedback about what's happening, a real automatic retry strategy that doesn't hammer a struggling backend, and the ability to take control manually at any point. This snippet implements genuine exponential backoff with a countdown that's driven by the *same* computed delay actually being waited on, not a decorative animation.

Exponential backoff, not a fixed retry interval

backoffDelayMs() computes each retry delay as 2000 * 2^attemptNumber — 2 seconds after the first failure, 4 seconds after the second, 8 after the third, and so on. Retrying at a constant fixed interval (say, always every 2 seconds) is a common but poor default: if the failure is caused by a struggling or overloaded backend, hammering it at a constant rate makes the underlying problem worse, while a backoff that gets progressively longer gives the backend genuine breathing room to recover between attempts.

The countdown shown to the user is the actual delay, not a fake animation

setFailedWithCountdown() takes the *real* computed backoff delay (converted to whole seconds) and counts down from exactly that number, updating the displayed number every second via setInterval. This is a small but important honesty detail: the countdown isn't a separate decorative timer running alongside some other actual retry schedule — it *is* the schedule, visibly counting down to the literal moment attemptLoad() will be called again.

A manual retry click resets the backoff sequence, deliberately

Clicking "Retry now" (or "Try again" after giving up) doesn't just trigger "attempt N+1" of the automatic sequence — when the give-up state has been reached, the click handler explicitly resets attempt back to 0 first. This is a deliberate distinction: an automatic retry happening because a timer expired is a passive continuation of the existing backoff sequence, while a user actively clicking a button is a fresh, deliberate signal of continued interest — treating it as a brand new attempt sequence (rather than the *nth* attempt of an already-exhausted backoff) avoids showing a confusingly short remaining-attempts count to someone who just explicitly asked to try again.

A genuine give-up state, not an infinite retry loop

After MAX_ATTEMPTS automatic attempts have failed, the loader stops retrying entirely and shows a clear "still unable to load" state with an explicit manual retry option — rather than continuing to silently retry forever with an ever-growing backoff delay that would eventually feel indistinguishable from the feature simply being broken. A bounded, clearly-communicated give-up point is what makes the manual retry option meaningful instead of superfluous.

Step by step

How to Use

  1. 1
    Watch the initial load attemptA standard spinner shows while the (simulated) request is in flight for the first time.
  2. 2
    Watch it fail and enter a countdownOn failure, the state switches to an error icon with a live countdown showing exactly how many seconds until the next automatic retry — driven by real exponential backoff.
  3. 3
    Let the countdown reach zeroThe next attempt fires automatically the instant the countdown hits 0 — the displayed number and the actual retry timing are the same value, not two separate systems.
  4. 4
    Click "Retry now" during a countdownSkips the wait and retries immediately, without needing to wait out the remaining backoff delay.
  5. 5
    Observe the give-up state after the max attemptsAfter MAX_ATTEMPTS failed automatic attempts, retrying stops and a clear "still unable to load" message appears with a manual retry button — clicking it starts a completely fresh attempt sequence.

Real-world uses

Common Use Cases

DASHBOARD
Dashboard and data-fetch loading states
Any initial page load fetching data from a backend benefits from graceful automatic retry instead of a dead-end error message.
Flaky or rate-limited API integrations
Third-party API calls prone to transient failures benefit from genuine backoff rather than hammering the same endpoint repeatedly.
Mobile apps on unreliable networks
Mobile connections drop and recover frequently — an automatic, visible retry sequence handles this far better than a static error screen.
REALTIME
Reconnecting real-time features
WebSocket or live-update features that lose connection benefit from the same backoff-with-visible-countdown pattern before reconnecting.
Related: Cascading Dependency Skeleton Loader — Parent Then Children, Honestly
See the Cascading Dependency Skeleton Loader — Parent Then Children, Honestly for a related loaders pattern worth pairing with this one.
Related: Route Tracking Progress Loader
See the Route Tracking Progress Loader for a related loaders pattern worth pairing with this one.
Related: Code Editor Skeleton Loader
See the Code Editor Skeleton Loader for a related loaders pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Exponential backoff (doubling the delay each time) avoids hammering a potentially struggling or overloaded backend at a constant rate. A progressively longer wait gives the backend genuine time to recover between attempts, which a fixed short interval does not.

It reflects the real timing exactly — the countdown starts from the actual computed backoff delay (converted to seconds) and the next attempt fires the instant it reaches zero. There is no separate "real" retry schedule running independently of what's displayed.

It immediately cancels the countdown interval and triggers a new attempt right away, without waiting for the remaining backoff delay to elapse — giving the user full manual control at any point during the automatic sequence.

No — after MAX_ATTEMPTS automatic attempts have failed, retrying stops entirely and a distinct "still unable to load" state appears with a manual retry option, rather than continuing an ever-growing backoff indefinitely.

The attempt counter is explicitly reset to zero before starting a new load — this is treated as a completely fresh attempt sequence, not a continuation of the already-exhausted automatic backoff, since a manual click represents deliberate renewed interest rather than a passive timer expiring.

Replace the body of simulateRequest() with your actual fetch or API call, keeping its Promise-based resolve/reject contract (resolving on success, rejecting on failure) — everything else, including the backoff timing, countdown display, and give-up logic, works unchanged against any request that follows that contract.