HTTP Retry Patterns — Complete Implementation Guide
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
Retrying 4xx errors -- A 400 Bad Request will always fail. Only retry 429 and 5xx status codes.
Ignoring Retry-After header -- The server explicitly tells you how long to wait. Ignoring it causes unnecessary failures.
Not setting timeouts -- Without timeouts, a hanging request blocks retry logic indefinitely.
Retrying non-idempotent methods without idempotency keys -- Retrying POST without keys creates duplicates.
Logging too much on retries -- Every retry produces logs. Aggregate retry information rather than logging each attempt verbosely.
Practice Questions
Which HTTP status codes should trigger a retry? 429 (Too Many Requests) and 5xx (Server Error). 4xx errors (except 429) should not be retried.
What is the Retry-After header? A header that tells the client how many seconds to wait before retrying. It is an HTTP standard.
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.
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
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