Skip to content

HTTP Retry Patterns — Complete Implementation Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about HTTP Retry Patterns. We cover key concepts, practical examples, and best practices to help you master this topic.

HTTP retry patterns automatically retry failed HTTP requests using status code analysis, retry-after headers, timeout handling, and exponential backoff for building reliable HTTP clients.

What You'll Learn

By the end of this tutorial, you will implement retry logic for HTTP clients, handle retry-after headers, distinguish retryable from non-retryable status codes, and set proper timeouts.

Why It Matters

HTTP is the backbone of microservice communication. Reliable HTTP clients with retry logic prevent transient network failures from causing cascading service disruptions.

Real-World Use

DodaTech's Microservices use a custom HTTP client that retries failed requests 3 times with exponential backoff, respecting Retry-After headers from rate-limited APIs.

HTTP Retry Learning Path

flowchart LR
  A[Idempotency] --> B[HTTP Retry]
  B --> C[Fetch API]
  B --> D[Axios]
  B --> E[Status Codes]
  B --> F{You Are Here}
  style F fill:#f90,color:#fff

Fetch with Retry

The Fetch API does not include built-in retry. Implement retry logic around fetch calls with proper error handling.

async function fetchWithRetry(url, options = {}, retries = 3) {
  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), options.timeout || 10000);

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

      clearTimeout(timeout);

      if (!response.ok && shouldRetry(response.status) && attempt < retries - 1) {
        const delay = getDelay(attempt, response);
        await sleep(delay);
        continue;
      }

      return response;
    } catch (err) {
      if (attempt === retries - 1) throw err;
      const delay = Math.min(200 * Math.pow(2, attempt), 10000);
      await sleep(delay);
    }
  }
}

function shouldRetry(status) {
  return status === 429 || status >= 500;
}

function getDelay(attempt, response) {
  const retryAfter = response.headers?.get("Retry-After");
  if (retryAfter) return parseInt(retryAfter) * 1000;
  return Math.min(200 * Math.pow(2, attempt), 30000);
}

function sleep(ms) {
  return new Promise(r => setTimeout(r, ms));
}

Axios Retry Interceptor

Axios interceptors provide a clean way to add retry logic to all requests made with the library.

npm install axios axios-retry
const axios = require("axios");
const axiosRetry = require("axios-retry");

const client = axios.create({
  timeout: 10000,
  headers: { "Content-Type": "application/json" }
});

axiosRetry(client, {
  retries: 3,
  retryDelay: (retryCount, error) => {
    if (error.response?.status === 429) {
      const retryAfter = error.response.headers["retry-after"];
      return retryAfter ? parseInt(retryAfter) * 1000 : 1000;
    }
    return axiosRetry.exponentialDelay(retryCount);
  },
  retryCondition: (error) => {
    return axiosRetry.isNetworkOrIdempotentRequestError(error) ||
      error.response?.status === 429 ||
      error.response?.status >= 500;
  },
  onRetry: (retryCount, error, requestConfig) => {
    console.log(`Retry ${retryCount} for ${requestConfig.url}: ${error.message}`);
  }
});

async function fetchData() {
  const response = await client.get("https://api.example.com/data");
  return response.data;
}

Handling Retry-After Header

The Retry-After header tells the client exactly how long to wait before retrying. Respecting it is critical for rate-limited APIs.

async function fetchWithRetryAfter(url, options = {}) {
  const maxRetries = options.maxRetries || 5;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 30000);

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

      clearTimeout(timeout);

      if (response.ok) return response;

      if (attempt === maxRetries - 1) {
        throw new Error(`Request failed after ${maxRetries} attempts`);
      }

      if (response.status === 429 || response.status >= 500) {
        const retryAfter = parseRetryAfter(response.headers);
        const delay = retryAfter || Math.min(200 * Math.pow(2, attempt), 60000);
        console.log(`HTTP ${response.status}: retrying in ${delay}ms (attempt ${attempt + 2})`);
        await new Promise(r => setTimeout(r, delay));
      } else {
        throw new Error(`HTTP ${response.status}: non-retryable error`);
      }

    } catch (err) {
      clearTimeout(timeout);
      if (err.name === "AbortError") {
        console.log(`Request timed out, retry ${attempt + 2}`);
        continue;
      }
      if (attempt === maxRetries - 1) throw err;
      await new Promise(r => setTimeout(r, Math.min(200 * Math.pow(2, attempt), 10000)));
    }
  }
}

function parseRetryAfter(headers) {
  const value = headers.get("Retry-After");
  if (!value) return null;
  const seconds = parseInt(value);
  if (!isNaN(seconds)) return seconds * 1000;
  return null;
}

Common Mistakes

  1. Retrying 4xx errors -- A 400 Bad Request will always fail. Only retry 429 and 5xx status codes.

  2. Ignoring Retry-After header -- The server explicitly tells you how long to wait. Ignoring it causes unnecessary failures.

  3. Not setting timeouts -- Without timeouts, a hanging request blocks retry logic indefinitely.

  4. Retrying non-idempotent methods without idempotency keys -- Retrying POST without keys creates duplicates.

  5. Logging too much on retries -- Every retry produces logs. Aggregate retry information rather than logging each attempt verbosely.

Practice Questions

  1. Which HTTP status codes should trigger a retry? 429 (Too Many Requests) and 5xx (Server Error). 4xx errors (except 429) should not be retried.

  2. What is the Retry-After header? A header that tells the client how many seconds to wait before retrying. It is an HTTP standard.

  3. Why should you implement retry at the HTTP client level rather than the application level? HTTP client-level retry is reusable across all API calls. Application-level retry must be duplicated for each integration.

  4. Challenge: Implement an HTTP retry that uses different strategies for different status codes.

function getRetryStrategy(status) {
  if (status === 429) return { maxRetries: 5, baseDelay: 1000, multiplier: 2 };
  if (status >= 500) return { maxRetries: 3, baseDelay: 500, multiplier: 3 };
  return { maxRetries: 0 };
}

FAQ

Should I retry POST requests?

Only with idempotency keys. Without them, retrying POST creates duplicate resources.

How do I prevent retry storms?

Use exponential backoff with jitter and respect Retry-After headers. Circuit breakers also help.

What is the best HTTP client for retry?

Axios with axios-retry is the most popular. Got and undici also have built-in retry support.

How do I handle DNS failures in retry?

DNS failures are typically non-transient from the client's perspective. Retry with full backoff, as DNS may propagate.

Can I retry WebSocket connections?

Yes, WebSocket reconnection uses similar backoff strategies. Many WebSocket libraries include auto-reconnection with backoff.

Mini Project

Build a complete HTTP retry client with exponential backoff, Retry-After handling, status-based decisions, timeouts, and logging.

class HTTPRetryClient {
  constructor(options = {}) {
    this.maxRetries = options.maxRetries || 3;
    this.baseDelay = options.baseDelay || 200;
    this.maxDelay = options.maxDelay || 30000;
    this.timeout = options.timeout || 10000;
  }

  async request(url, options = {}) {
    const method = (options.method || "GET").toUpperCase();

    for (let attempt = 0; attempt < this.maxRetries; attempt++) {
      try {
        const result = await this.makeRequest(url, options);

        if (result.ok || attempt === this.maxRetries - 1) {
          return result;
        }

        if (!this.shouldRetry(result.status, method)) {
          return result;
        }

        const delay = this.calculateDelay(attempt, result.headers);
        await this.sleep(delay);

      } catch (err) {
        if (attempt === this.maxRetries - 1) throw err;
        const delay = Math.min(this.baseDelay * Math.pow(2, attempt), this.maxDelay);
        await this.sleep(delay);
      }
    }
  }

  async makeRequest(url, options) {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), this.timeout);
    try {
      return await fetch(url, { ...options, signal: controller.signal });
    } finally {
      clearTimeout(timer);
    }
  }

  shouldRetry(status, method) {
    if (method !== "GET" && method !== "HEAD" && method !== "PUT" && method !== "DELETE") {
      return false;
    }
    return status === 429 || status >= 500;
  }

  calculateDelay(attempt, headers) {
    const retryAfter = headers?.get("retry-after");
    if (retryAfter) return parseInt(retryAfter) * 1000;
    return Math.min(this.baseDelay * Math.pow(2, attempt), this.maxDelay);
  }

  sleep(ms) {
    return new Promise(r => setTimeout(r, ms));
  }
}

What's Next

Now that you understand HTTP retry patterns, explore retrying database operations. Then learn about retrying async message processing.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro