Advanced Retry Patterns — Complete Implementation Guide
In this tutorial, you will learn about Advanced Retry Patterns. We cover key concepts, practical examples, and best practices to help you master this topic.
Advanced retry patterns go beyond simple retry loops, implementing conditional chains, hedging (racing multiple requests), fallback strategies, and self-tuning retry rates.
What You'll Learn
By the end of this tutorial, you will implement hedging requests for latency-sensitive operations, conditional retry chains for complex workflows, and adaptive retry rates.
Why It Matters
Simple retries work for basic scenarios. Advanced patterns handle complex failure modes and optimize for both latency and reliability in high-stakes operations.
Real-World Use
DodaTech's payment service uses hedging: it sends the same request to two payment providers and uses the first response, with retries as a fallback.
Advanced Retry Learning Path
flowchart LR
A[Best Practices] --> B[Advanced Retry]
B --> C[Hedging]
B --> D[Fallback]
B --> E[Adaptive]
B --> F{You Are Here}
style F fill:#f90,color:#fff
Hedging Requests
Hedging sends the same request to multiple replicas and uses the first successful response. This reduces tail latency.
async function hedgeRequest(url, options = {}) {
const replicas = options.replicas || 2;
const timeout = options.timeout || 2000;
const requests = Array.from({ length: replicas }, (_, i) =>
fetch(url, { signal: AbortSignal.timeout(timeout) })
.then(async r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
);
try {
return await Promise.race(requests);
} catch {
// If all hedged requests fail, fall back to regular retry
return retryRequest(url, options);
}
}
// Expected behavior: Two identical requests are sent.
// The first to respond (successfully) wins.
// If both fail, fall back to standard retry.
Conditional Retry Chains
Different failures may need different retry strategies. A conditional chain selects the right Strategy based on the error.
class ConditionalRetryChain {
constructor() {
this.strategies = [];
}
when(condition, strategy) {
this.strategies.push({ condition, strategy });
return this;
}
getStrategy(err) {
for (const { condition, strategy } of this.strategies) {
if (condition(err)) return strategy;
}
return { maxRetries: 0 };
}
async execute(fn) {
for (let attempt = 0; attempt < 5; attempt++) {
try {
return await fn();
} catch (err) {
const strategy = this.getStrategy(err);
if (attempt >= strategy.maxRetries - 1) throw err;
const delay = strategy.delay ? strategy.delay(attempt) : 200 * Math.pow(2, attempt);
await new Promise(r => setTimeout(r, delay));
}
}
}
}
const chain = new ConditionalRetryChain();
chain
.when(
(err) => err.status === 429,
{ maxRetries: 5, delay: (a) => 1000 * Math.pow(2, a) }
)
.when(
(err) => err.code === "ECONNREFUSED",
{ maxRetries: 3, delay: (a) => 500 * Math.pow(2, a) }
)
.when(
(err) => err.code === "40001",
{ maxRetries: 2, delay: (a) => 100 }
);
Retry with Fallback
When retries are exhausted, provide a degraded response rather than failing completely.
class RetryWithFallback {
constructor(options = {}) {
this.maxRetries = options.maxRetries || 3;
this.fallback = options.fallback;
}
async execute(primaryFn, fallbackFn) {
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
try {
return await primaryFn();
} catch (err) {
if (attempt === this.maxRetries - 1) {
if (fallbackFn) {
console.log("Primary failed, using fallback");
return fallbackFn();
}
throw err;
}
await sleep(200 * Math.pow(2, attempt));
}
}
}
}
// Usage
const result = await retry.execute(
() => fetchFromPrimary(),
() => fetchFromCache()
);
Adaptive Retry Rate
Monitor success rates and adjust retry aggressiveness dynamically based on current system conditions.
class AdaptiveRetry {
constructor() {
this.window = [];
this.windowSize = 100;
this.baseDelay = 200;
this.multiplier = 1;
}
record(success) {
this.window.push({ time: Date.now(), success });
if (this.window.length > this.windowSize) {
this.window.shift();
}
this.updateMultiplier();
}
updateMultiplier() {
const recent = this.window.filter(
r => Date.now() - r.time < 60000
);
if (recent.length < 10) return;
const failures = recent.filter(r => !r.success).length;
const failureRate = failures / recent.length;
if (failureRate > 0.5) {
this.multiplier = Math.min(10, this.multiplier * 2);
} else if (failureRate < 0.1) {
this.multiplier = Math.max(1, this.multiplier * 0.5);
}
}
async execute(fn) {
for (let attempt = 0; attempt < 3; attempt++) {
try {
const result = await fn();
this.record(true);
return result;
} catch (err) {
this.record(false);
if (attempt === 2) throw err;
const delay = this.baseDelay * Math.pow(2, attempt) * this.multiplier;
await sleep(Math.min(delay, 60000));
}
}
}
}
Common Mistakes
Hedging without cancellation -- Hedged requests that are not cancelled waste server resources. Use AbortController to cancel losers.
Using fallback as primary retry -- Fallbacks should be degraded responses, not full retries. Overusing fallbacks hides real problems.
Conditional chains with overlapping conditions -- Ensure conditions are mutually exclusive or order-dependent. First match wins.
Adaptive retry with too-small Windows -- Small windows react to noise. Use at least 100 samples for adaptive decisions.
Hedging for non-idempotent operations -- Hedging sends multiple requests. Only hedge idempotent read operations.
Practice Questions
What is a hedging request? Sending the same request to multiple replicas and using the first successful response. It reduces tail latency.
When should you use a fallback instead of retrying? When the primary source is clearly unavailable (circuit open) and a degraded response is better than an error.
How does adaptive retry adjust to system conditions? It monitors success rates and increases backoff when failure rates are high, reducing retry pressure on struggling systems.
Challenge: Implement hedging that cancels remaining requests after the first success.
async function hedgeWithCancel(urls, timeout = 2000) {
const controllers = urls.map(() => new AbortController());
const requests = urls.map((url, i) =>
fetch(url, { signal: controllers[i].signal })
.then(r => {
controllers.forEach((c, j) => { if (j !== i) c.abort(); });
return r.json();
})
);
return Promise.race(requests);
}
FAQ
Mini Project
Build an advanced retry system with hedging, conditional chains, fallback, and adaptive rates.
class AdvancedRetrySystem {
constructor() {
this.hedgingEnabled = true;
this.adaptive = new AdaptiveRetry();
this.fallbackCache = new Map();
}
async execute(request, options = {}) {
const { idempotent, timeout = 2000 } = options;
// Phase 1: Hedge (for idempotent reads)
if (idempotent && this.hedgingEnabled) {
try {
return await this.hedge(request, timeout);
} catch {
// Fall through to retry
}
}
// Phase 2: Adaptive retry
return this.adaptive.execute(request);
}
async hedge(fn, timeout) {
const results = await Promise.allSettled([
fn(),
fn(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("timeout")), timeout)
)
]);
const success = results.find(r => r.status === "fulfilled");
if (success) return success.value;
throw new Error("All hedged requests failed");
}
async retryWithFallback(primary, fallback) {
for (let i = 0; i < 3; i++) {
try { return await primary(); }
catch { if (i === 2) return fallback(); }
await sleep(200 * Math.pow(2, i));
}
}
}
What's Next
Now that you understand advanced retry, apply everything in the comprehensive retry project. Then explore circuit breaker pattern.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro