typescript
51 lines · 8 steps
Retrying HTTP requests with backoff
A generic wrapper that retries failed requests on transient errors using exponential backoff with jitter.
Explained by
highlit
1type RetryableRequest<T> = () => Promise<T>;
2
3interface RetryOptions {
4 maxAttempts?: number;
5 baseDelayMs?: number;
6 retryOnStatus?: number[];
7}
8
9class HttpError extends Error {
10 constructor(public readonly status: number, message: string) {
11 super(message);
12 this.name = "HttpError";
13 }
14}
15
16const isHttpError = (err: unknown): err is HttpError =>
17 err instanceof HttpError;
18
19const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
20
21export async function withRetry<T>(
22 request: RetryableRequest<T>,
23 options: RetryOptions = {},
24): Promise<T> {
25 const {
26 maxAttempts = 3,
27 baseDelayMs = 200,
28 retryOnStatus = [429, 502, 503, 504],
29 } = options;
30
31 let lastError: unknown;
32
33 for (let attempt = 1; attempt <= maxAttempts; attempt++) {
34 try {
35 return await request();
36 } catch (err) {
37 lastError = err;
38
39 const retryable = isHttpError(err) && retryOnStatus.includes(err.status);
40 if (!retryable || attempt === maxAttempts) {
41 throw err;
42 }
43
44 const jitter = Math.random() * baseDelayMs;
45 const delay = baseDelayMs * 2 ** (attempt - 1) + jitter;
46 await sleep(delay);
47 }
48 }
49
50 throw lastError;
51}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Only retry on transient failures — inspect the error type and status before deciding to try again.
- 2Exponential backoff with random jitter spreads out retries and avoids synchronized thundering-herd load.
- 3A generic wrapper keeps retry logic reusable across any promise-returning request without coupling to a specific client.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
rust
use serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(untagged)]
Parsing flexible JSON shapes with serde
deserialization
enums
json
Intermediate
6 steps
ruby
require "shellwords" require "open3" module Backup
Building safe shell commands in Ruby
shell-out
subprocess
command-injection
Intermediate
7 steps
python
from fastapi import FastAPI, WebSocket, WebSocketDisconnect app = FastAPI()
Building a WebSocket chat with FastAPI
websockets
broadcast
connection-management
Intermediate
9 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/retrying-http-requests-with-backoff-explained-typescript-6954/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.