Examples
Real-world examples showing how to use Polite Retry in different scenarios.
Fetch API
Basic example using the Fetch API:
import { retry } from 'polite-retry';
async function fetchWithRetry(url: string) {
return retry(
async () => {
const response = await fetch(url);
// Don't retry client errors (4xx)
if (response.status >= 400 && response.status < 500) {
const error = new Error(`Client error: ${response.status}`);
(error as any).noRetry = true;
throw error;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
},
{
maxRetries: 3,
initialDelayMs: 100,
jitter: 'full',
retryIf: (error: any) => !error.noRetry,
onRetry: (error, attempt) => {
console.log(`Retry ${attempt}: ${error.message}`);
}
}
);
}
// Usage
const data = await fetchWithRetry('https://api.example.com/users');
Axios
Using Polite Retry with Axios:
import axios, { AxiosError } from 'axios';
import { retry, AdaptiveRetryBudget } from 'polite-retry';
// Create a budget per API
const apiBudget = new AdaptiveRetryBudget({ initialBudget: 0.2 });
const apiClient = axios.create({
baseURL: 'https://api.example.com',
timeout: 5000,
});
async function request<T>(config: Parameters<typeof axios.request>[0]): Promise<T> {
return retry(
async () => {
const response = await apiClient.request<T>(config);
return response.data;
},
{
maxRetries: 3,
jitter: 'full',
retryIf: (error) => {
if (error instanceof AxiosError) {
// Retry network errors and 5xx responses
return !error.response || error.response.status >= 500;
}
return true;
}
}
);
}
// Usage
const user = await request<User>({ method: 'GET', url: '/users/123' });
Database Queries
Retrying database operations with connection issues:
import { retry } from 'polite-retry';
import { Pool } from 'pg';
const pool = new Pool();
async function queryWithRetry<T>(sql: string, params: any[] = []): Promise<T[]> {
return retry(
async () => {
const client = await pool.connect();
try {
const result = await client.query(sql, params);
return result.rows;
} finally {
client.release();
}
},
{
maxRetries: 3,
initialDelayMs: 100,
jitter: 'equal',
retryIf: (error) => {
// Retry connection errors, not query errors
const code = (error as any).code;
return ['ECONNREFUSED', 'ETIMEDOUT', 'ECONNRESET', '57P01'].includes(code);
}
}
);
}
// Usage
const users = await queryWithRetry<User>('SELECT * FROM users WHERE active = $1', [true]);
Microservice Client
A complete microservice client with all protections:
import {
retryWithProtection,
CircuitBreaker,
AdaptiveRetryBudget,
BackpressureManager,
CircuitOpenError
} from 'polite-retry';
// One instance per downstream service
class ServiceClient {
private circuitBreaker: CircuitBreaker;
private budget: AdaptiveRetryBudget;
private backpressure: BackpressureManager;
constructor(
private baseUrl: string,
private serviceName: string
) {
this.backpressure = new BackpressureManager();
this.circuitBreaker = new CircuitBreaker({
failureThreshold: 0.5,
windowSize: 20,
resetTimeoutMs: 30000,
onStateChange: (state) => {
console.log(`[${serviceName}] Circuit: ${state}`);
metrics.gauge(`circuit.${serviceName}`, state === 'open' ? 0 : 1);
}
});
this.budget = new AdaptiveRetryBudget({
initialBudget: 0.2,
checkBackpressure: () => this.backpressure.isOverloaded(serviceName),
onBudgetChange: (budget) => {
metrics.gauge(`retry_budget.${serviceName}`, budget);
}
});
}
async request<T>(path: string, options: RequestInit = {}): Promise<T> {
try {
return await retryWithProtection(
async () => {
const response = await fetch(`${this.baseUrl}${path}`, {
...options,
signal: AbortSignal.timeout(5000),
});
// Record backpressure from response
this.backpressure.recordFromHeaders(this.serviceName, response.headers);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
},
{
circuitBreaker: this.circuitBreaker,
budget: this.budget
},
{
maxRetries: 3,
jitter: 'full',
retryIf: (error) => {
// Don't retry client errors
return !error.message.includes('4');
}
}
);
} catch (error) {
if (error instanceof CircuitOpenError) {
// Fast fail - circuit is open
throw new Error(`${this.serviceName} is unavailable`);
}
throw error;
}
}
getMetrics() {
return {
circuitState: this.circuitBreaker.getState(),
failureRate: this.circuitBreaker.getFailureRate(),
retryBudget: this.budget.getBudget(),
...this.budget.getMetrics()
};
}
dispose() {
this.budget.dispose();
}
}
// Usage
const userService = new ServiceClient('https://user-service', 'user-service');
const paymentService = new ServiceClient('https://payment-service', 'payment-service');
const user = await userService.request<User>('/users/123');
const payment = await paymentService.request<Payment>('/charge', {
method: 'POST',
body: JSON.stringify({ amount: 100 })
});
API Gateway Pattern
Using Polite Retry in an API gateway that proxies to multiple services:
import express from 'express';
import {
retryWithBudget,
AdaptiveRetryBudget,
RequestCounter,
createBackpressureMiddleware
} from 'polite-retry';
const app = express();
// Track our own load for backpressure responses
const counter = new RequestCounter();
app.use(counter.middleware());
app.use(createBackpressureMiddleware({
getLoadLevel: () => counter.getCount() / 100,
overloadThreshold: 0.8,
}));
// Create budgets for each upstream service
const serviceBudgets = new Map<string, AdaptiveRetryBudget>();
function getBudget(service: string): AdaptiveRetryBudget {
if (!serviceBudgets.has(service)) {
serviceBudgets.set(service, new AdaptiveRetryBudget({
initialBudget: 0.15,
highFailureThreshold: 0.25,
}));
}
return serviceBudgets.get(service)!;
}
// Proxy endpoint
app.all('/api/:service/*', async (req, res) => {
const { service } = req.params;
const path = req.params[0];
const budget = getBudget(service);
try {
const result = await retryWithBudget(
async () => {
const response = await fetch(`http://${service}/${path}`, {
method: req.method,
headers: req.headers as any,
body: ['POST', 'PUT', 'PATCH'].includes(req.method)
? JSON.stringify(req.body)
: undefined,
});
if (!response.ok) {
throw new Error(`Upstream error: ${response.status}`);
}
return response.json();
},
budget,
{ maxRetries: 2, jitter: 'full' }
);
res.json(result);
} catch (error) {
res.status(502).json({
error: 'Service unavailable',
service
});
}
});
// Metrics endpoint
app.get('/metrics', (req, res) => {
const metrics: Record<string, any> = {};
for (const [service, budget] of serviceBudgets) {
metrics[service] = budget.getMetrics();
}
res.json(metrics);
});
app.listen(3000);
Queue Consumer
Processing messages from a queue with retry protection:
import { retryWithBudget, AdaptiveRetryBudget } from 'polite-retry';
import { SQS } from '@aws-sdk/client-sqs';
const sqs = new SQS();
// Budget for external API calls during message processing
const apiBudget = new AdaptiveRetryBudget({ initialBudget: 0.2 });
async function processMessage(message: any): Promise<void> {
const data = JSON.parse(message.Body);
// Retry external API calls, not the whole message processing
await retryWithBudget(
async () => {
await fetch('https://external-api.com/webhook', {
method: 'POST',
body: JSON.stringify(data),
});
},
apiBudget,
{ maxRetries: 3, jitter: 'full' }
);
}
async function pollQueue(): Promise<void> {
while (true) {
const response = await sqs.receiveMessage({
QueueUrl: process.env.QUEUE_URL,
MaxNumberOfMessages: 10,
WaitTimeSeconds: 20,
});
if (response.Messages) {
await Promise.all(
response.Messages.map(async (message) => {
try {
await processMessage(message);
await sqs.deleteMessage({
QueueUrl: process.env.QUEUE_URL,
ReceiptHandle: message.ReceiptHandle!,
});
} catch (error) {
console.error('Failed to process message:', error);
// Message will be retried by SQS after visibility timeout
}
})
);
}
// Log metrics periodically
console.log('API Budget Metrics:', apiBudget.getMetrics());
}
}
pollQueue();
Per-Service Configuration
Different retry configurations for different services:
import { retry, retryWithBudget, AdaptiveRetryBudget } from 'polite-retry';
// Critical payment service - conservative retries
const paymentConfig = {
maxRetries: 2,
initialDelayMs: 500,
jitter: 'full' as const,
timeoutMs: 10000,
};
// Fast cache service - quick retries
const cacheConfig = {
maxRetries: 1,
initialDelayMs: 10,
jitter: 'full' as const,
timeoutMs: 100,
};
// Slow analytics service - patient retries
const analyticsConfig = {
maxRetries: 5,
initialDelayMs: 1000,
maxDelayMs: 60000,
jitter: 'decorrelated' as const,
timeoutMs: 30000,
};
// Budgets with different thresholds
const budgets = {
payment: new AdaptiveRetryBudget({
initialBudget: 0.1, // Very conservative
highFailureThreshold: 0.1,
}),
cache: new AdaptiveRetryBudget({
initialBudget: 0.3, // More aggressive
highFailureThreshold: 0.5,
}),
analytics: new AdaptiveRetryBudget({
initialBudget: 0.2,
highFailureThreshold: 0.3,
}),
};
// Usage
await retryWithBudget(() => chargeCard(), budgets.payment, paymentConfig);
await retry(() => getFromCache(key), cacheConfig);
await retryWithBudget(() => sendAnalytics(data), budgets.analytics, analyticsConfig);
With Prometheus Monitoring
Integrating with Prometheus for observability:
import { AdaptiveRetryBudget, CircuitBreaker } from 'polite-retry';
import { Gauge, Counter, register } from 'prom-client';
// Prometheus metrics
const retryBudgetGauge = new Gauge({
name: 'retry_budget',
help: 'Current retry budget',
labelNames: ['service'],
});
const failureRateGauge = new Gauge({
name: 'retry_failure_rate',
help: 'Current failure rate',
labelNames: ['service'],
});
const rafGauge = new Gauge({
name: 'retry_amplification_factor',
help: 'Retry amplification factor',
labelNames: ['service'],
});
const circuitStateGauge = new Gauge({
name: 'circuit_breaker_state',
help: 'Circuit breaker state (1=closed, 0=open)',
labelNames: ['service'],
});
const retryCounter = new Counter({
name: 'retry_attempts_total',
help: 'Total retry attempts',
labelNames: ['service', 'success'],
});
// Factory function with monitoring
function createMonitoredClient(serviceName: string) {
const budget = new AdaptiveRetryBudget({
initialBudget: 0.2,
onBudgetChange: (b, rate) => {
retryBudgetGauge.set({ service: serviceName }, b);
failureRateGauge.set({ service: serviceName }, rate);
},
});
const breaker = new CircuitBreaker({
onStateChange: (state) => {
circuitStateGauge.set(
{ service: serviceName },
state === 'closed' ? 1 : 0
);
},
});
// Periodically update RAF metric
setInterval(() => {
const metrics = budget.getMetrics();
rafGauge.set({ service: serviceName }, metrics.retryAmplificationFactor);
}, 10000);
return { budget, breaker };
}
// Express endpoint for Prometheus scraping
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});
Full Stack Example
Complete example with server-side backpressure and client-side retry:
Server (Express)
// server.ts
import express from 'express';
import { RequestCounter, createBackpressureMiddleware } from 'polite-retry';
const app = express();
const counter = new RequestCounter();
// Add backpressure headers to all responses
app.use(counter.middleware());
app.use(createBackpressureMiddleware({
getLoadLevel: () => counter.getCount() / 50, // Max 50 concurrent
overloadThreshold: 0.8,
retryAfterSeconds: 2,
}));
app.get('/api/data', async (req, res) => {
// Simulate work
await new Promise(r => setTimeout(r, 100));
res.json({ data: 'Hello!' });
});
app.listen(3000, () => console.log('Server running on :3000'));
Client
// client.ts
import {
retryWithBudget,
AdaptiveRetryBudget,
BackpressureManager
} from 'polite-retry';
const backpressure = new BackpressureManager();
const budget = new AdaptiveRetryBudget({
initialBudget: 0.2,
checkBackpressure: () => backpressure.isOverloaded('api'),
});
async function fetchData() {
return retryWithBudget(
async () => {
const response = await fetch('http://localhost:3000/api/data');
// Record backpressure signal
backpressure.recordFromHeaders('api', response.headers);
// Check if server is telling us to back off
const retryAfter = backpressure.getRetryAfterMs('api');
if (retryAfter) {
console.log(`Server suggests waiting ${retryAfter}ms`);
}
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
},
budget,
{ maxRetries: 3, jitter: 'full' }
);
}
// Make many concurrent requests
async function loadTest() {
const results = await Promise.allSettled(
Array.from({ length: 100 }, () => fetchData())
);
const succeeded = results.filter(r => r.status === 'fulfilled').length;
console.log(`Success: ${succeeded}/100`);
console.log('Metrics:', budget.getMetrics());
}
loadTest();