Transient network failures — DNS blips, cloud provider hiccups, brief overloads — are normal in distributed systems. Without retry logic, a single dropped packet causes a user-visible failure on an operation that would succeed 200ms later. ISO 25010 reliability.recoverability requires the application to restore a defined performance level after failure; immediate hard failures with no retry violate that objective. Equally important: retrying non-idempotent POST requests without user confirmation risks duplicate charges, duplicate submissions, or duplicate order creation — a common and costly AI-coding defect.
Medium because missing retry logic converts recoverable transient failures into permanent user-visible errors, and naive retry on non-idempotent calls risks duplicate transactions.
Implement retry with exponential backoff and jitter. Only auto-retry idempotent requests (GET, safe PUT). For POST/DELETE, surface a retry button to the user instead.
// lib/retry.ts
export async function withRetry<T>(
fn: () => Promise<T>,
{ maxAttempts = 3, baseDelayMs = 200 } = {}
): Promise<T> {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await fn()
} catch (err) {
if (attempt === maxAttempts - 1) throw err
const delay = baseDelayMs * 2 ** attempt + Math.random() * baseDelayMs
await new Promise((r) => setTimeout(r, delay))
}
}
throw new Error('unreachable')
}
Jitter (the Math.random() term) prevents thundering-herd when multiple clients retry simultaneously after a shared outage.
ID: error-resilience.network-api-resilience.retry-logic-backoff
Severity: medium
What to look for: Count all API call sites that could fail transiently. Enumerate which implement retry logic with exponential backoff vs. which fail immediately. Search for retry logic in API calls. Look for exponential backoff implementation (wait 100ms, then 200ms, then 400ms, etc.). Check whether non-idempotent requests (POST, PUT, DELETE) are excluded from auto-retry or require user confirmation.
Pass criteria: Retry logic is implemented with exponential backoff and jitter. Non-idempotent requests are not auto-retried without user confirmation. At least 50% of API integrations should implement retry with exponential backoff (at least 3 retries with increasing delays).
Fail criteria: No retry logic found, or retries use fixed delay, or non-idempotent requests are auto-retried without safeguards.
Skip (N/A) when: The application has no API calls or only calls read-only endpoints.
Cross-reference: For circuit breaker patterns, see circuit-breaker-pattern. For form submission retry, see form-submission-retry.
Detail on fail: "No retry logic implemented. Network failures result in immediate failure" or "POST requests auto-retry without user confirmation, risking duplicate charges or submissions"
Remediation: Implement retry logic with exponential backoff:
// lib/retry.ts — exponential backoff
async function withRetry<T>(fn: () => Promise<T>, retries = 3): Promise<T> {
for (let i = 0; i < retries; i++) { try { return await fn() } catch { await new Promise(r => setTimeout(r, 1000 * 2 ** i)) } }
return fn()
}
// lib/retry.ts
async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxAttempts = 3,
baseDelay = 100
): Promise<T> {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await fn()
} catch (error) {
if (attempt === maxAttempts - 1) throw error
const delay = baseDelay * Math.pow(2, attempt)
const jitter = Math.random() * delay * 0.1
await new Promise(resolve =>
setTimeout(resolve, delay + jitter)
)
}
}
}
// Usage - only for idempotent requests:
const data = await retryWithBackoff(() =>
fetch('/api/data').then(r => r.json())
)