Skip to content

Advanced Retry Patterns — Complete Implementation Guide

DodaTech Updated 2026-06-28 6 min read

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

  1. Hedging without cancellation -- Hedged requests that are not cancelled waste server resources. Use AbortController to cancel losers.

  2. Using fallback as primary retry -- Fallbacks should be degraded responses, not full retries. Overusing fallbacks hides real problems.

  3. Conditional chains with overlapping conditions -- Ensure conditions are mutually exclusive or order-dependent. First match wins.

  4. Adaptive retry with too-small Windows -- Small windows react to noise. Use at least 100 samples for adaptive decisions.

  5. Hedging for non-idempotent operations -- Hedging sends multiple requests. Only hedge idempotent read operations.

Practice Questions

  1. What is a hedging request? Sending the same request to multiple replicas and using the first successful response. It reduces tail latency.

  2. 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.

  3. 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.

  4. 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

Does hedging increase server load?

Yes. Hedging sends duplicate requests. Only use it for critical, low-traffic operations.

How do I choose between hedging and retry?

Use hedging for latency-sensitive read operations. Use retry for write operations and non-critical reads.

Can I combine hedging and retry?

Yes. Hedge first for low latency. If all hedged requests fail, fall back to retry with backoff.

What is the ideal fallback response?

A cached response, a default value, or a degraded but functional response. Never return stale or incorrect data.

How adaptive should retry be?

Start with fixed configuration. Add adaptivity when you have enough traffic data to tune the parameters.

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