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.
