TypeScript · Zero runtime dependencies · npm · Source

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();