B2B platform for digital goods

Retry/Backoff Patterns for B2B Integrations 2026: Polly, axios-retry, tenacity

Production retry/backoff patterns for B2B integrations: exponential + jitter, circuit breaker, and code samples in 3 languages — with FoxReload-specific guidance.

Retry/Backoff Patterns for B2B Integrations 2026: Polly, axios-retry, tenacity

Retry is not "wrap in try/catch inside a while loop". The right strategy depends on the error class, the expected SLA of the upstream service, and current load during recovery. This article is a practical reference for retry patterns in B2B integrations, with production code samples and FoxReload-specific guidance.

1. Base formula: exponential + jitter

function delay(attempt: number, baseMs = 500, capMs = 30000): number {
  const exp = Math.min(baseMs * Math.pow(2, attempt), capMs);
  const jitter = Math.random() * exp * 0.5; // ±50%
  return exp / 2 + jitter; // "full jitter" AWS-style
}
// attempt 0: 250–750ms
// attempt 1: 500–1500ms
// attempt 2: 1000–3000ms
// attempt 5: 8000–24000ms

"Full jitter" (above) is AWS-recommended and cuts peak recovery load better than "equal jitter".

2. When to retry and when not to

HTTP status Retryable? Comment
5xx Yes Server-side transient
429 Yes Respect the Retry-After header if present
408 Yes Client timeout
4xx (rest) No Permanent client error
Network error Yes DNS, conn reset, TLS
Timeout on GET Yes Safe — GET is idempotent
Timeout on POST Special See section 3

3. Special case: POST /api/orders timeout

FoxReload has no idempotency keys. A timed-out POST /api/orders may have created an order that is now processing. Before retrying the POST:

async function safeRetryOrder(
  items: OrderItem[],
  apiKey: string,
  attempt: number,
): Promise<Order> {
  if (attempt > 0) {
    // Check if a previous attempt already created the order
    const recent = await fetch('https://public-api.foxreload.com/api/orders?limit=10', {
      headers: { 'X-API-Key': apiKey },
    }).then(r => r.json());
    const match = recent.orders?.find((o: Order) =>
      o.items.some(i => i.product.id === items[0].itemId) &&
      Date.now() - new Date(o.createdAt).getTime() < 120_000,
    );
    if (match) return match; // order exists — do not create again
  }
  return fetch('https://public-api.foxreload.com/api/orders', {
    method: 'POST',
    headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' },
    body: JSON.stringify({ items }),
  }).then(r => r.json());
}

4. axios-retry (Node.js)

import axios from 'axios';
import axiosRetry from 'axios-retry';

const client = axios.create({
  baseURL: 'https://public-api.foxreload.com',
  timeout: 30000,
  headers: { 'X-API-Key': process.env.FOXRELOAD_KEY },
});

axiosRetry(client, {
  retries: 5,
  retryDelay: (count) => axiosRetry.exponentialDelay(count) + Math.random() * 1000,
  retryCondition: (err) => {
    if (!err.response) return true; // network
    const s = err.response.status;
    // Do NOT automatically retry POST — handle separately
    if (err.config?.method === 'post') return false;
    return s >= 500 || s === 429 || s === 408;
  },
  shouldResetTimeout: true,
});

shouldResetTimeout: true is essential — without it one 30s timeout consumes the whole retry budget.

5. tenacity (Python)

from tenacity import retry, stop_after_attempt, wait_exponential_jitter, retry_if_exception_type
import httpx

@retry(
    stop=stop_after_attempt(6),
    wait=wait_exponential_jitter(initial=0.5, max=30, jitter=2),
    retry=retry_if_exception_type((httpx.TransportError, httpx.HTTPStatusError)),
    reraise=True,
)
def get_order_status(order_id, api_key):
    r = httpx.get(
        f'https://public-api.foxreload.com/api/orders/{order_id}',
        headers={'X-API-Key': api_key},
        timeout=30,
    )
    if r.status_code >= 500 or r.status_code == 429:
        r.raise_for_status()
    return r.json()

For POST /api/orders, implement the status-check-before-retry pattern manually rather than using a generic retry decorator.

6. Polly (C#/.NET)

var policy = Policy
    .HandleResult<HttpResponseMessage>(r => (int)r.StatusCode >= 500 || (int)r.StatusCode == 429)
    .Or<HttpRequestException>()
    .WaitAndRetryAsync(5, attempt =>
        TimeSpan.FromMilliseconds(500 * Math.Pow(2, attempt)
            + Random.Shared.Next(0, 500)));

var circuit = Policy
    .HandleResult<HttpResponseMessage>(r => (int)r.StatusCode >= 500)
    .CircuitBreakerAsync(5, TimeSpan.FromSeconds(60));

var combined = Policy.WrapAsync(circuit, policy);
// Use for GET requests only; handle POST /api/orders separately
var resp = await combined.ExecuteAsync(() =>
    client.GetAsync($"/api/orders/{orderId}"));

7. Circuit breaker — when retries no longer help

If upstream fails 50%+ for 30 seconds, open the breaker and fail fast:

import CircuitBreaker from 'opossum';
const breaker = new CircuitBreaker(callFoxreload, {
  errorThresholdPercentage: 50,
  resetTimeout: 60_000,
  rollingCountTimeout: 30_000,
});
breaker.fallback(() => ({ fromCache: true }));

This protects downstream from a retry avalanche during an outage.

CTA

The FoxReload API returns Retry-After on 429s and uses HTTPS across all endpoints. Full retry recommendations are in the onboarding doc after requesting access.

Frequently asked questions

How many retry attempts are optimal for a B2B API?
5–8 attempts with a total budget under 5 minutes. More and the client sees a timeout; fewer and you lose orders on transient errors. For real-time endpoints (catalog, products) — 3 retries over 30s.
What should I never retry?
Any 4xx except 408 (timeout) and 429 (rate limit). 400 bad request, 401 unauthorised, 403 forbidden, 404 not found, 422 validation error are permanent errors. Retrying them is an infinite loop with no chance of success.
Why add jitter to backoff?
Without jitter every client retries in sync (at 30s, 60s, 120s) — a thundering herd during recovery. ±30% jitter spreads retry waves over 25–35s, 50–70s, 100–140s. It cuts peak load on recovery 4–6×.
Is a circuit breaker useful if I already have retries?
Yes. Retries handle isolated errors (network blip). The circuit breaker handles sustained outages — after 50% failures in 30s the breaker opens, further requests fail immediately and don't load the upstream. Recovery: half-open canary in 60s.
How do I safely retry POST /api/orders after a timeout?
FoxReload has no idempotency keys. Before retrying, call GET /api/orders?limit=10 to check if the order was created. If it exists, poll it. If not, retry the POST. Never blindly retry a POST to /api/orders — you will create duplicate orders and be charged twice.
Get FoxReload API access

Related articles