Error Handling Patterns

Timeout Budget Patterns for Microservices

How to implement timeout budgets across microservice chains to prevent cascading timeouts and give every downstream call a fair time slice.

The Timeout Problem in Microservices

In a microservice architecture, a single user request might fan out to 5–10 downstream services. Each service sets its own timeout — say 5 seconds. If Service A calls Service B which calls Service C, the effective timeout chain can be 15 seconds, causing the user to wait long past when they've already given up or reconnected.

Worse: Service C may still be processing a request for a user who abandoned it 10 seconds ago, burning resources needlessly.

What Is a Timeout Budget?

A timeout budget (also called a deadline) is a total time allowance for an entire request chain. It is established at the edge and propagated downstream. Each service consumes from the budget; when the budget is exhausted, all in-progress work is cancelled.

User Request → API Gateway (budget: 10s)
  → Service A (budget: 10s, takes 2s, passes remaining 8s downstream)
    → Service B (budget: 8s, takes 3s, passes remaining 5s downstream)
      → Service C (budget: 5s)

If Service C doesn't respond in 5 seconds, the request is cancelled — not after a compounding 15-second chain.

Propagating Deadlines with gRPC

gRPC has first-class deadline support built in:

import grpc
from datetime import datetime, timedelta

# Establish deadline at the edge
deadline = datetime.utcnow() + timedelta(seconds=10)

# Each call propagates the same deadline
channel = grpc.insecure_channel('service-a:50051')
stub = ServiceAStub(channel)
response = stub.GetData(
    request,
    timeout=(deadline - datetime.utcnow()).total_seconds(),
)

gRPC automatically cancels in-flight RPCs when the deadline passes, propagating cancellation signals to all downstream services.

HTTP Timeout Headers

For HTTP services, pass the remaining budget as a header:

import httpx
import time

def call_downstream(
    url: str,
    remaining_budget_seconds: float,
) -> httpx.Response:
    # Reserve 10% for overhead (serialization, network)
    downstream_timeout = remaining_budget_seconds * 0.9
    return httpx.get(
        url,
        timeout=downstream_timeout,
        headers={'X-Request-Timeout': str(remaining_budget_seconds)},
    )

The X-Request-Timeout header (or a custom header like X-Deadline-Unix-Timestamp) lets the downstream service know how much time remains and avoid starting work it cannot complete.

Cascading Failures Without Budgets

Without timeout budgets:

  • Service C becomes slow (100ms → 8s per request)
  • Service B's threads block waiting for Service C
  • Service B's thread pool exhausts
  • Service A starts timing out waiting for Service B
  • The API Gateway starts timing out
  • Users see failures everywhere — even for endpoints that don't use Service C

With timeout budgets, Service C's slowness is contained: requests time out quickly, resources are freed, and degradation is limited to endpoints that actually depend on Service C.

Implementation Checklist

  • [ ] Set a global budget at the API gateway or edge service
  • [ ] Propagate the remaining budget to every downstream call
  • [ ] Reserve headroom (10–20%) at each hop for serialization overhead
  • [ ] Use gRPC deadlines or X-Request-Deadline headers for HTTP
  • [ ] Check the remaining budget before starting expensive work
  • [ ] Return 408 Request Timeout or 504 Gateway Timeout when budget exhausted
  • [ ] Emit metrics on budget-exceeded events per service

Summary

Timeout budgets prevent cascading failures by giving the entire request chain a single time budget that shrinks at each hop. Use gRPC's built-in deadline propagation for gRPC services, and pass remaining budget via custom headers for HTTP services. Always check the budget before starting expensive work, and emit metrics when budgets are exhausted to identify slow dependencies.

البروتوكولات ذات الصلة

مصطلحات ذات صلة

المزيد في Error Handling Patterns