Timeouts, retries, and circuit breakers
When you call a downstream service, a few patterns keep you safe. A timeout puts an upper bound on how long you wait. A retry with backoff handles transient errors. A circuit breaker stops hammering a dependency that is clearly down. Together, they are the minimum resilience kit for any service that calls another one.
async function fetchWithTimeout(url: string, ms: number): Promise<Response> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), ms);
try {
return await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timer);
}
}Every outbound call has a timeout. Unbounded waits are bugs.
Retries need exponential backoff and a maximum number of attempts. Without backoff, you turn one failure into a thundering herd. Without a cap, you retry forever. Start with 3 attempts and a doubling delay: 100ms, 200ms, 400ms. Only retry on errors that might succeed if tried again: network errors, 503s, rate limits. Never retry 400s or 401s; they are not going to fix themselves.
A circuit breaker watches the error rate to a downstream service. If too many calls fail in a short window, the breaker opens and starts failing fast without even attempting the call. After a cool-down period, it half-opens and lets a few calls through to see if the dependency recovered. This protects both you and the downstream from a dogpile of retries during an outage.
Only if the endpoint is idempotent. An idempotent endpoint gives the same result no matter how many times you call it, usually because you pass an idempotency key. If the downstream API is idempotent, retries are safe. If not, do not retry mutations blindly. This is exactly the Stripe-style idempotency key pattern you will see in the Accelerator course.
Quiz: Quiz
Loading practice…
Checkpoint: Contracts and resilience checkpoint
Loading practice…