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.

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.

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
408 Yes Client timeout
4xx (rest) No Permanent client error
Network error Yes DNS, conn reset, TLS
Timeout Yes Careful β€” request may have completed

Special case for POST timeouts: only retry with an idempotency key, otherwise you create duplicates.

3. axios-retry (Node.js)

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

const client = axios.create({ baseURL: 'https://api.foxreload.com', timeout: 30000 });
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;
    return s >= 500 || s === 429 || s === 408;
  },
  shouldResetTimeout: true,
});

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

4. 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 create_order(payload, idem_key):
    r = httpx.post('https://api.foxreload.com/v1/orders',
                   json=payload,
                   headers={'Idempotency-Key': idem_key},
                   timeout=30)
    if r.status_code >= 500 or r.status_code == 429:
        r.raise_for_status()
    return r.json()

5. 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);
var resp = await combined.ExecuteAsync(() => client.PostAsync("/v1/orders", body));

6. 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 HTTP/2 across 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. FoxReload retry endpoints cap at 8 retries per 24h. For real-time endpoints (catalog, balance) β€” 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.
Get FoxReload API access

Related articles