imization; it is a requirement for systems operating above 99.9% availability.
Core Solution
Production retry patterns require explicit error classification, randomized backoff, idempotency enforcement, and downstream health awareness. The following implementation demonstrates a TypeScript-native retry utility that adheres to SRE best practices.
Step 1: Define Retry Configuration & Error Classification
export interface RetryConfig {
maxAttempts: number;
baseDelayMs: number;
maxDelayMs: number;
jitterFactor: number; // 0.0 to 1.0
retryableStatusCodes: number[];
retryableErrorTypes: string[];
}
export const DEFAULT_RETRY_CONFIG: RetryConfig = {
maxAttempts: 3,
baseDelayMs: 200,
maxDelayMs: 5000,
jitterFactor: 0.5,
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
retryableErrorTypes: ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'FetchError'],
};
Step 2: Implement Core Retry Logic with Exponential Backoff & Jitter
export async function withRetry<T>(
fn: () => Promise<T>,
config: Partial<RetryConfig> = {}
): Promise<T> {
const cfg = { ...DEFAULT_RETRY_CONFIG, ...config };
let attempt = 0;
while (attempt < cfg.maxAttempts) {
try {
return await fn();
} catch (error: any) {
attempt++;
const isRetryable =
cfg.retryableStatusCodes.includes(error?.status ?? error?.response?.status) ||
cfg.retryableErrorTypes.includes(error?.name ?? error?.code);
if (!isRetryable || attempt >= cfg.maxAttempts) {
throw error;
}
const exponentialDelay = Math.min(
cfg.baseDelayMs * Math.pow(2, attempt - 1),
cfg.maxDelayMs
);
const jitter = exponentialDelay * cfg.jitterFactor * Math.random();
const delay = exponentialDelay + jitter;
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw new Error('Retry limit exceeded');
}
Step 3: Architecture Decisions & Rationale
Error Classification over Blind Retries: Retrying 400 Bad Request or 401 Unauthorized errors wastes cycles and masks configuration bugs. Explicit status code and error type filtering ensures only transient conditions trigger retries.
Jitter Strategy: Full jitter (delay * random()) or equal jitter (base + random()) breaks synchronization across concurrent clients. The implementation uses multiplicative jitter to preserve exponential growth while randomizing execution windows. This prevents thundering herd effects during partial outages.
Idempotency Enforcement: Retries are only safe for idempotent operations. For POST or PUT requests, the caller must attach an idempotency key (UUID) to the request header. The downstream service must deduplicate based on this key. Without idempotency, retries introduce data corruption, duplicate charges, or inconsistent state.
Circuit Breaker Integration: Retry logic should not operate in isolation. At the service boundary, a circuit breaker tracks failure rates and opens when thresholds are exceeded. When open, retries are short-circuited, and fallback logic executes. This prevents waste during full downstream outages and allows recovery time.
Observability Hooks: Production systems must emit retry attempt counts, success/failure ratios, and delay distributions. Instrument the utility with metrics exporters (Prometheus, OpenTelemetry) to detect retry storms early.
Pitfall Guide
1. Retrying Non-Idempotent Operations
Retrying a state-mutating endpoint without idempotency guarantees causes duplicate side effects. Production fix: Enforce idempotency keys at the API contract level. Reject retries that lack them. Use PUT/PATCH for updates, reserve POST for explicitly idempotent or key-driven operations.
2. Omitting Jitter
Fixed or deterministic delays synchronize retry waves across clients. During a 503 spike, 100 clients retrying at exactly 1s, 2s, 4s creates predictable load peaks that delay recovery. Production fix: Always apply randomization. Equal or full jitter is non-negotiable in distributed environments.
3. Retrying 4xx Client Errors
400, 401, 403, and 422 errors indicate client-side misconfiguration or authorization failures. Retrying them consumes resources and obscures root causes. Production fix: Classify errors explicitly. Log 4xx failures immediately. Implement alerting on repeated client errors to catch configuration drift.
4. Ignoring Circuit Breaker Thresholds
Retrying indefinitely during a full downstream outage extends the failure window. The circuit breaker must sit upstream of retry logic. Production fix: Integrate with a circuit breaker that tracks failure rate, slow call ratio, and timeout count. Open the circuit when thresholds breach, execute fallbacks, and allow half-open probing.
5. Hardcoding Retry Counts Without Observability
Static maxAttempts: 3 works in staging but fails under variable load. Some services require 2 attempts; others need 5 with longer backoff. Production fix: Externalize retry configuration. Track retry ratios per endpoint. Adjust thresholds based on SLOs and downstream capacity. Alert when retry success rate drops below 80%.
6. Retrying During Full Outages Without Fallbacks
When a dependency is completely down, retries delay graceful degradation. Production fix: Implement fallback strategies (cached responses, default values, queue-and-retry). Use circuit breakers to switch to fallback mode. Ensure fallbacks do not violate data consistency guarantees.
Best Practices from Production:
- Classify errors before retrying; never retry on client errors
- Apply jitter to every backoff calculation
- Enforce idempotency for all mutating operations
- Couple retries with circuit breakers at the service boundary
- Emit metrics for attempt counts, delays, and success rates
- Test retry behavior with chaos engineering (latency injection, packet loss)
- Document retry semantics in API contracts to align client/server expectations
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Payment processing / financial mutations | Idempotency key + Circuit Breaker + 2 attempts | Prevents duplicate charges; circuit breaker blocks retries during processor outages | Low (reduces chargebacks and reconciliation costs) |
| Cache miss / read-heavy endpoint | Exponential Backoff + Jitter + 3 attempts | Transient failures are common; retries improve hit rates without overloading origin | Moderate (increases compute but reduces origin load) |
| Third-party SaaS API | Adaptive Retry + Rate Limit Awareness + Circuit Breaker | External services enforce strict limits; adaptive patterns respect backoff headers | High (avoids account suspension and overage fees) |
| Internal microservice call | Circuit Breaker + Fallback + 2 attempts | High internal reliability; fast failure preferred over prolonged retries | Low (reduces latency and thread pool exhaustion) |
Configuration Template
// retry.config.ts
import { RetryConfig } from './retry.types';
export const RETRY_PROFILES: Record<string, Partial<RetryConfig>> = {
read: {
maxAttempts: 3,
baseDelayMs: 150,
maxDelayMs: 3000,
jitterFactor: 0.6,
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
retryableErrorTypes: ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED'],
},
write: {
maxAttempts: 2,
baseDelayMs: 300,
maxDelayMs: 5000,
jitterFactor: 0.5,
retryableStatusCodes: [408, 429, 503, 504],
retryableErrorTypes: ['ECONNRESET', 'ETIMEDOUT'],
},
external: {
maxAttempts: 4,
baseDelayMs: 500,
maxDelayMs: 10000,
jitterFactor: 0.7,
retryableStatusCodes: [429, 500, 502, 503, 504],
retryableErrorTypes: ['ECONNRESET', 'ETIMEDOUT', 'FetchError'],
},
};
Quick Start Guide
- Install dependencies: Ensure your project supports
async/await and has a metrics/exporter library (OpenTelemetry, Prometheus client, or cloud-native equivalent).
- Add the retry utility: Copy the
withRetry function and DEFAULT_RETRY_CONFIG into your shared utilities module. Export the interface and configuration profiles.
- Wrap external calls: Replace direct
fetch/axios/DB client calls with await withRetry(() => client.request(), RETRY_PROFILES.read). Attach idempotency keys for write operations.
- Instrument and validate: Add counters for
retry.attempt, retry.success, and retry.delay. Deploy to staging, inject latency using chaos tools, and verify that p99 latency and downstream request spikes align with expected thresholds.