Guides

Reliability guide

Dependency Resilience: Timeouts, Retries, Backpressure, and Fallbacks

A dependency call is part of your service's failure domain. Bound the time, work, and retries it can consume before an upstream incident becomes your outage.

Most production failures are not a single process crash. A database slows down, DNS resolution stalls, a third-party API returns errors, or a region loses capacity. If callers wait indefinitely, retry without limits, and scale into the same constrained dependency, a partial fault becomes a cascading outage.

Dependency resilience is the set of explicit decisions that prevent that cascade. It includes timeouts, cancellation, retry policy, concurrency limits, circuit breaking, queues, fallbacks, and telemetry. These controls are application behavior, not a sidecar or Kubernetes setting that can be enabled blindly.

Start with a dependency map and latency budget

For every request path, map each synchronous and asynchronous dependency: DNS, edge or ingress, service calls, database, cache, queue, identity provider, payment system, and external API. Record ownership, protocol, normal and tail latency, error behavior, quota, connection limit, data criticality, and fallback option.

Then set an end-to-end deadline from the user-facing entry point. Divide it across the work that must happen. A 1,000 ms user budget cannot contain three serial 500 ms default client timeouts. Leave time for queuing, serialization, and a response; do not allocate the entire budget to downstream calls.

Propagate deadlines and cancellation through service calls. When the client has left or the upstream deadline expires, continuing expensive work consumes capacity that cannot improve the response. Use monotonic clocks for elapsed-time calculations and distinguish connection, TLS handshake, first-byte, idle, and overall request timeouts when the client library supports them.

Retry only when it is safe and affordable

Retries can improve transient failure recovery, but each retry adds load to a dependency that may already be overloaded. Retry only operations that are idempotent or protected by an idempotency key. Never retry a payment, message publication, or mutation simply because the client did not receive a response; first know whether the server may have completed it.

Use a small retry count, exponential backoff, jitter, and a maximum elapsed time smaller than the caller's remaining deadline. Honor Retry-After where applicable. Establish a retry budget, such as a small percentage of successful traffic, so a failing upstream cannot multiply your request volume indefinitely.

Avoid retrying at multiple layers. If the edge, gateway, service mesh, SDK, and application each retry three times, one original request can become dozens of dependency calls. Assign retry ownership to one layer per operation and observe retry attempts separately from user requests.

A timeout is a capacity control

An unbounded wait holds a worker, connection, memory, and often a request slot. A sensible deadline protects the latency budget and leaves capacity for requests that can still succeed. Tune it from measured tail latency and product requirements, not a copied default.

Limit concurrency and apply backpressure

Every dependency client needs a maximum number of concurrent calls and a bounded queue. Once the limit is reached, choose a deliberate response: queue work with a deadline, return a cached result, shed low-priority traffic, or fail fast with a retryable error. An unbounded internal queue converts a brief upstream slowdown into memory pressure and long tail latency.

Size connection pools for the dependency and fleet, not simply for one container. At scale, per-pod pools can overwhelm a database or NAT gateway. Track active and idle connections, acquisition wait time, rejected work, and the number of replicas. A horizontal autoscaler that sees high CPU or queue depth can add pressure unless it is coordinated with dependency capacity.

Use asynchronous queues for work that does not need to finish in the user response. Set maximum age, retry and dead-letter policy, consumer concurrency, and idempotent processing. Queues provide buffering, not infinite capacity: a rising age is a user-impact signal and needs a load-shedding or recovery decision.

Use circuit breakers and fallbacks carefully

A circuit breaker opens when failure or latency crosses a defined threshold, preventing more calls while the dependency recovers. It should have a bounded open interval and a limited half-open test. Scope breakers by dependency and operation; one slow optional recommendation API should not block checkout.

Fallback behavior must preserve correctness. Good fallbacks include serving a known-safe cached catalog, accepting a request into a durable queue, or hiding a nonessential widget. Bad fallbacks include serving stale authorization data, silently dropping a financial action, or returning a success response for work that was never recorded.

For state-changing work, use patterns that make recovery explicit: idempotency keys, transactional outbox records, sagas with compensating actions, and reconciliation jobs. A circuit breaker improves availability only when the product can make a truthful response while the dependency is unavailable.

Design for cloud-native and multi-region realities

Kubernetes restarts failed containers, but it does not know whether retrying an external API is safe or whether a database has reached its connection limit. Configure readiness to reflect whether a replica can serve safely, but do not turn a shared dependency fault into a liveness failure that restarts all callers.

Deploy replicas across failure domains when the service requires it, and test the behavior when a zone, DNS resolver, credential issuer, or regional dependency fails. Multi-region routing adds its own concerns: replication lag, write ownership, data residency, health-check quality, and cache invalidation. Sending a request to a healthy region is not useful if that region cannot safely serve the required data.

At the edge, set origin connect and response timeouts, retry rules, and failover criteria to match application semantics. Restrict automatic retries to safe methods or explicitly idempotent requests. Cacheable responses can be served closer to users during an origin impairment, while personalized and mutation paths need an honest degraded response or controlled queue. Monitor latency by region and route, not just global averages.

Instrument the whole call chain

Use OpenTelemetry to create spans for dependency calls and propagate trace context across HTTP, gRPC, and messaging boundaries. Record operation name, target system, status, duration, retry count, timeout reason, and safe route attributes. Do not put access tokens, full query values, or personal data into span attributes.

Combine traces with metrics: dependency request rate, availability, p50/p95/p99 duration, timeout count, retry count, circuit state, queue age, pool wait time, and saturation. Correlate these with deploys, configuration changes, and edge routing events. The goal is to tell whether users are waiting on application code, a shared dependency, or the network path.

Exercise failure before it exercises you

Run controlled tests in a safe environment. Add latency, return 429 and 503 responses, blackhole a route, exhaust a connection pool, delay a queue consumer, and rotate a dependency credential. Confirm that deadlines bound the request, retries do not surge, fallbacks are truthful, alerts fire on user impact, and recovery does not require manual repair of duplicate work.

Document the decision for each critical dependency: owner, SLO, timeout, retry rule, concurrency limit, fallback, data-loss risk, and escalation path. Review it whenever the dependency, traffic profile, or regional architecture changes.

Authoritative references

Make origin dependencies less visible to users

Optimi can help evaluate edge caching, routing, and origin-protection patterns that complement application-level resilience controls.

Discuss resilient delivery