Timeout Patterns in Synchronous Communication
Deadline Propagation
Read aloud is unavailable in this browser.
From Relative Timeouts to Absolute Deadlines
In the previous page, we explored propagating remaining timeout budgets across service boundaries. While effective, relative timeout propagation has a fundamental limitation: time keeps passing during transmission. When Service A propagates "5000ms remaining" to Service B, some of that time is consumed by the network hop itself. By the time Service B reads the header, the actual remaining time is already less than 5000ms.
Deadline propagation addresses this by communicating an absolute point in time rather than a relative duration. Instead of "you have 5 seconds," the message becomes "complete by 2024-01-15T10:30:45.123Z." This approach provides stronger guarantees and enables more sophisticated coordination patterns in distributed systems.
By the end of this page, you will understand the advantages of deadline propagation over relative timeouts, learn how to implement deadline-based timing across services, understand the role of clock synchronization, and see how deadlines enable advanced patterns like deadline-aware scheduling.
Deadline vs. Timeout: Understanding the Difference
Though often used interchangeably in casual conversation, deadline and timeout represent distinct concepts:
Timeout (relative):
- "Wait for 5 seconds, then give up"
- Duration relative to the current moment
- Clock starts when the operation begins
- Value degrades as time passes (but the timeout holder doesn't know by how much)
Deadline (absolute):
- "Complete by 10:30:45 UTC, or give up"
- Fixed point in time, independent of when you check it
- Remaining time calculable at any moment:
deadline - now - Unambiguous across all participants who share a synchronized clock
| Aspect | Timeout (Relative) | Deadline (Absolute) |
|---|---|---|
| Representation | Duration in ms/seconds | Timestamp (ISO 8601, Unix epoch) |
| Calculation of remaining | Unknown after propagation | deadline - current_time |
| Network latency impact | Remaining overstated | Automatically accounted for |
| Clock dependency | None (local timer) | Requires synchronized clocks |
| Propagation accuracy | Degrades at each hop | Constant (modulo clock skew) |
| Common usage | HTTP client timeouts | gRPC deadlines, distributed scheduling |
The network latency problem with relative timeouts:
Consider this scenario with relative timeout propagation:
T+0ms: Service A sends request to B with "remaining: 5000ms"
T+50ms: Request traverses network
T+50ms: Service B receives request, reads "remaining: 5000ms"
Service B sets its timeout to 5000ms
BUT: Service A only has 4950ms remaining!
T+4950ms: Service A's deadline expires
T+5050ms: Service B's timeout expires
100ms of wasted work on Service B
With deadline propagation:
T+0ms: Now = 10:30:40.000, Deadline = 10:30:45.000
Service A sends request to B with "deadline: 10:30:45.000"
T+50ms: Request traverses network
T+50ms: Now = 10:30:40.050
Service B receives request, reads deadline
Calculates remaining: 10:30:45.000 - 10:30:40.050 = 4950ms
Sets timeout to 4950ms (accurate!)
T+4950ms: Both services agree the deadline is reached
The key insight is that deadlines are self-correcting for network latency. No matter how long the network hop takes, the receiving service can calculate the true remaining time by comparing the deadline to its current clock reading. This assumes clocks are synchronized—we'll address that challenge shortly.
Implementing Deadline Propagation
Implementing deadline propagation involves three core components:
- Deadline establishment: Setting the initial deadline at the system edge
- Deadline transmission: Passing the deadline across service boundaries
- Deadline enforcement: Respecting the deadline at each service
Let's examine each component in detail.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
// Deadline propagation implementationimport { Request, Response, NextFunction } from 'express'; const DEADLINE_HEADER = 'x-request-deadline';const DEFAULT_DEADLINE_MS = 30000; interface DeadlineContext { deadline: Date; isExpired(): boolean; getRemainingMs(): number; toISOString(): string;} // Create deadline context from absolute deadlinefunction createDeadlineContext(deadline: Date): DeadlineContext { return { deadline, isExpired: () => Date.now() >= deadline.getTime(), getRemainingMs: () => Math.max(0, deadline.getTime() - Date.now()), toISOString: () => deadline.toISOString(), };} // Middleware: Extract or establish deadlineexport function deadlineMiddleware( req: Request, res: Response, next: NextFunction): void { let deadline: Date; // Check for incoming deadline header const deadlineHeader = req.get(DEADLINE_HEADER); if (deadlineHeader) { const parsed = Date.parse(deadlineHeader); if (!isNaN(parsed)) { deadline = new Date(parsed); } else { // Invalid deadline, establish new one deadline = new Date(Date.now() + DEFAULT_DEADLINE_MS); } } else { // No deadline provided, establish one deadline = new Date(Date.now() + DEFAULT_DEADLINE_MS); } // Check if deadline is already expired if (Date.now() >= deadline.getTime()) { res.status(504).json({ error: 'Deadline exceeded before processing', deadline: deadline.toISOString(), }); return; } // Attach context to request req.deadlineContext = createDeadlineContext(deadline); // Set response timeout const remainingMs = req.deadlineContext.getRemainingMs(); res.setTimeout(remainingMs); next();} // Decorator: Deadline-aware function executionasync function withDeadline<T>( deadline: DeadlineContext, operation: () => Promise<T>, operationName: string = 'operation'): Promise<T> { // Check deadline before starting if (deadline.isExpired()) { throw new DeadlineExceededError( `Deadline expired before starting ${operationName}`, deadline.deadline ); } const remainingMs = deadline.getRemainingMs(); // Create timeout race return Promise.race([ operation(), new Promise<never>((_, reject) => { setTimeout(() => { reject(new DeadlineExceededError( `Deadline exceeded during ${operationName}`, deadline.deadline )); }, remainingMs); }), ]);} // HTTP client that propagates deadlinesclass DeadlineAwareHttpClient { constructor(private baseUrl: string) {} async request( path: string, options: RequestInit, deadline: DeadlineContext ): Promise<Response> { // Check if we have enough time const remainingMs = deadline.getRemainingMs(); const minRequired = 100; // Minimum ms to attempt a request if (remainingMs < minRequired) { throw new DeadlineExceededError( 'Insufficient time for downstream request', deadline.deadline ); } // Add deadline header const headers = new Headers(options.headers); headers.set(DEADLINE_HEADER, deadline.toISOString()); // Create abort controller for timeout const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), remainingMs); try { return await fetch(`${this.baseUrl}${path}`, { ...options, headers, signal: controller.signal, }); } finally { clearTimeout(timeoutId); } }} // Custom error classclass DeadlineExceededError extends Error { constructor( message: string, public readonly deadline: Date, public readonly exceededBy: number = Date.now() - deadline.getTime() ) { super(message); this.name = 'DeadlineExceededError'; }}When transmitting deadlines, use ISO 8601 format with millisecond or microsecond precision (e.g., 2024-01-15T10:30:45.123Z). Avoid formats that only support second precision—a one-second error in deadline transmission is significant for many operations.
Clock Synchronization: The Foundation
Deadline propagation's accuracy depends entirely on clock synchronization between services. If Server A and Server B disagree about the current time, they'll compute different remaining durations from the same deadline.
The clock skew problem:
Deadline: 10:30:45.000 UTC
Server A's clock: 10:30:40.000 (accurate)
→ Remaining: 5000ms
Server B's clock: 10:30:42.000 (2 seconds fast)
→ Remaining: 3000ms
Server C's clock: 10:30:38.000 (2 seconds slow)
→ Remaining: 7000ms
Server B will timeout 2 seconds early; Server C will work 2 seconds past the actual deadline. Neither is correct.
Clock synchronization solutions:
- •How it works: Clients query NTP servers to determine clock offset and adjust their system time.
- •Typical accuracy: 1-10 milliseconds on local networks; 10-100 milliseconds over internet.
- •Best practice: Use multiple NTP servers for redundancy; prefer cloud provider's NTP service for cloud VMs.
- •Limitation: NTP can be disrupted by network issues; clock can drift between sync intervals.
- •How it works: Hardware-assisted timestamping for sub-microsecond accuracy.
- •Typical accuracy: Sub-microsecond with hardware support; microseconds with software.
- •Use case: Required for latency-critical applications (financial trading, telecom).
- •Limitation: Requires PTP-capable hardware and network infrastructure.
- •AWS: Amazon Time Sync Service (NTP at 169.254.169.123)
- •GCP: Google's leap-smeared NTP; also offers Spanner TrueTime API for bounded uncertainty
- •Azure: Windows Time Service with Azure-local NTP servers
- •Best practice: Always use cloud provider's time service within their cloud; external NTP adds latency and uncertainty.
Handling clock skew gracefully:
Even with NTP, some clock skew is inevitable. Design your deadline propagation to be tolerant:
- Buffer for skew: Subtract expected maximum skew from deadlines before making decisions
- Monitor skew: Track clock offset between services and alert if it exceeds thresholds
- Fallback to relative: If clock sync cannot be guaranteed, fall back to relative timeout propagation
- Bounded uncertainty: Some systems (like Google Spanner's TrueTime) provide uncertainty bounds on clock readings
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
// Skew-tolerant deadline handlingconst EXPECTED_CLOCK_SKEW_MS = 50; // Conservative estimate for NTP-synced servers interface SkewTolerantDeadlineContext { deadline: Date; assumedSkewMs: number; // Conservative remaining time (accounts for possible skew) getRemainingMsConservative(): number; // Optimistic remaining time (ignores skew) getRemainingMsOptimistic(): number; // Check if deadline is definitely expired (even with skew) isDefinitelyExpired(): boolean; // Check if deadline might be expired (considering skew) isPossiblyExpired(): boolean;} function createSkewTolerantContext( deadline: Date, assumedSkewMs: number = EXPECTED_CLOCK_SKEW_MS): SkewTolerantDeadlineContext { return { deadline, assumedSkewMs, getRemainingMsConservative() { // Assume our clock might be slow, so deadline might be sooner return Math.max(0, deadline.getTime() - Date.now() - assumedSkewMs); }, getRemainingMsOptimistic() { // Assume our clock might be fast, so we might have more time return Math.max(0, deadline.getTime() - Date.now() + assumedSkewMs); }, isDefinitelyExpired() { // Even if our clock is maximally fast, deadline has passed return Date.now() - assumedSkewMs >= deadline.getTime(); }, isPossiblyExpired() { // Our clock might be slow, so deadline might have passed return Date.now() + assumedSkewMs >= deadline.getTime(); }, };} // Usage exampleasync function handleWithSkewTolerance( ctx: SkewTolerantDeadlineContext, operation: () => Promise<void>): Promise<void> { // Use conservative estimate for timeouts const conservativeRemaining = ctx.getRemainingMsConservative(); if (conservativeRemaining < 100) { throw new Error('Deadline too close (with clock skew consideration)'); } // Log the uncertainty console.log(`Deadline remaining: ${ctx.getRemainingMsOptimistic()}ms to ${conservativeRemaining}ms (±${ctx.assumedSkewMs}ms)`); // Set timeout using conservative estimate const timeoutHandle = setTimeout(() => { throw new DeadlineExceededError('Conservative deadline exceeded', ctx.deadline); }, conservativeRemaining); try { await operation(); } finally { clearTimeout(timeoutHandle); }}If your infrastructure doesn't have reliable clock synchronization (common in on-premise deployments or edge computing), deadline propagation can be dangerous. A server with a slow clock will work past deadlines; a server with a fast clock will timeout early. Verify NTP is working before relying on absolute deadlines.
gRPC's Deadline Model: A Reference Implementation
gRPC provides the most mature and widely-adopted implementation of deadline propagation. Understanding how gRPC handles deadlines offers valuable lessons for implementing deadlines in other protocols.
How gRPC deadlines work:
- Client sets deadline: The client creates a context with a deadline (absolute time) or timeout (relative duration)
- Transmission: gRPC converts the deadline to a relative timeout for transmission (the
grpc-timeoutheader) - Server reception: Server receives the timeout, converts back to an absolute deadline in its local time
- Automatic propagation: When the server makes downstream gRPC calls with the same context, the deadline automatically propagates
- Cancellation: If the deadline expires, the RPC is automatically cancelled on both sides
The grpc-timeout header:
gRPC transmits deadlines as relative timeouts in a compact format:
5000m= 5000 milliseconds5S= 5 seconds5M= 5 minutes5H= 5 hours
This relative format avoids clock synchronization issues during transmission—the receiving server computes the deadline based on its own clock.
Why gRPC uses relative over absolute:
- Clock skew tolerance: Servers don't need synchronized clocks for the transmission itself
- Simpler parsing: Integer + unit is simpler than parsing ISO 8601
- Bounded transmission overhead: The timeout value doesn't grow unboundedly like timestamps
However, internally, gRPC uses absolute deadlines (context.WithDeadline). The conversion to relative happens only at the wire protocol level.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
package main import ( "context" "log" "time" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" pb "example.com/order/proto") func main() { conn, err := grpc.Dial("order-service:50051", grpc.WithInsecure()) if err != nil { log.Fatalf("Failed to connect: %v", err) } defer conn.Close() client := pb.NewOrderServiceClient(conn) // Option 1: Set deadline (absolute) deadline := time.Now().Add(5 * time.Second) ctx, cancel := context.WithDeadline(context.Background(), deadline) defer cancel() // Option 2: Set timeout (relative, converted to deadline internally) // ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) // Make the RPC - deadline is automatically included resp, err := client.PlaceOrder(ctx, &pb.OrderRequest{ Items: []*pb.Item{{Id: "item-1", Quantity: 2}}, }) if err != nil { // Check for deadline exceeded if status.Code(err) == codes.DeadlineExceeded { log.Println("Order placement timed out") return } // Check if our local context was cancelled if ctx.Err() == context.DeadlineExceeded { log.Println("Client deadline exceeded before response") return } log.Fatalf("Order failed: %v", err) } log.Printf("Order placed: %v", resp.OrderId)}If you don't set a deadline, gRPC RPCs can wait indefinitely. This is a common source of resource leaks in gRPC systems. Always set a deadline for every outbound RPC, and consider using interceptors to enforce default deadlines.
Deadline-Aware Application Design
Beyond basic propagation, deadlines enable sophisticated application patterns. Truly deadline-aware systems can make intelligent decisions about how to use their remaining time budget.
Pattern 1: Deadline-based operation selection
Choose operations based on available time:
1234567891011121314151617181920212223242526272829303132333435363738394041424344
interface RecommendationOptions { fast: { maxLatencyMs: 100, quality: 'basic' }; standard: { maxLatencyMs: 500, quality: 'good' }; comprehensive: { maxLatencyMs: 2000, quality: 'best' };} async function getRecommendations( userId: string, deadline: DeadlineContext): Promise<Recommendations> { const remainingMs = deadline.getRemainingMs(); // Select strategy based on available time if (remainingMs >= 2000) { // Plenty of time - use ML model with full feature set return comprehensiveRecommendations(userId); } else if (remainingMs >= 500) { // Moderate time - use cached model results + light personalization return standardRecommendations(userId); } else if (remainingMs >= 100) { // Limited time - return cached popular items return fastRecommendations(userId); } else { // No time - skip recommendations entirely return { items: [], source: 'skipped-due-to-deadline' }; }} // In the handler:async function productPageHandler(req: Request): Promise<Response> { const product = await getProduct(req.productId); // Always needed // Only include recommendations if we have time const remainingAfterProduct = req.deadlineContext.getRemainingMs(); const recommendations = await getRecommendations( req.userId, req.deadlineContext ); return { product, recommendations, };}Pattern 2: Deadline-aware retry decisions
Adjust retry behavior based on remaining time:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
interface RetryConfig { maxAttempts: number; initialDelayMs: number; maxDelayMs: number; backoffMultiplier: number;} async function executeWithDeadlineAwareRetry<T>( operation: () => Promise<T>, config: RetryConfig, deadline: DeadlineContext, minTimeForAttemptMs: number = 500): Promise<T> { let lastError: Error | null = null; let delay = config.initialDelayMs; for (let attempt = 1; attempt <= config.maxAttempts; attempt++) { const remainingMs = deadline.getRemainingMs(); // Check if we have enough time for another attempt if (remainingMs < minTimeForAttemptMs) { throw new DeadlineExceededError( `Not enough time for attempt ${attempt}. Remaining: ${remainingMs}ms, Required: ${minTimeForAttemptMs}ms`, deadline.deadline ); } try { // Cap operation timeout at remaining time const attemptTimeout = Math.min( remainingMs - 100, // Leave buffer for retry logic 5000 // Maximum per-attempt timeout ); return await Promise.race([ operation(), new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Attempt timeout')), attemptTimeout) ), ]); } catch (error) { lastError = error as Error; // Check if we should retry const remainingAfterAttempt = deadline.getRemainingMs(); const nextDelay = Math.min(delay, remainingAfterAttempt - minTimeForAttemptMs); if ( attempt < config.maxAttempts && nextDelay > 0 && remainingAfterAttempt > minTimeForAttemptMs + nextDelay ) { console.log( `Attempt ${attempt} failed, retrying in ${nextDelay}ms. ` + `Remaining time: ${remainingAfterAttempt}ms` ); await sleep(nextDelay); delay = Math.min(delay * config.backoffMultiplier, config.maxDelayMs); } else { // No more time for retries break; } } } throw lastError || new Error('All retry attempts exhausted');}Pattern 3: Deadline-based queue prioritization
When processing from a queue, prioritize items with imminent deadlines:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
interface QueueItem<T> { data: T; deadline: Date; enqueuedAt: Date;} class DeadlineAwarePriorityQueue<T> { private items: QueueItem<T>[] = []; enqueue(data: T, deadline: Date): void { this.items.push({ data, deadline, enqueuedAt: new Date(), }); // Sort by deadline (earliest first) this.items.sort((a, b) => a.deadline.getTime() - b.deadline.getTime()); } dequeue(): QueueItem<T> | null { // Remove expired items const now = Date.now(); this.items = this.items.filter(item => item.deadline.getTime() > now); // Return the item with earliest deadline return this.items.shift() || null; } // Get item with tightest deadline that has enough time to process dequeueWithMinTime(minRequiredMs: number): QueueItem<T> | null { const now = Date.now(); for (let i = 0; i < this.items.length; i++) { const item = this.items[i]; const remaining = item.deadline.getTime() - now; if (remaining >= minRequiredMs) { this.items.splice(i, 1); return item; } } return null; } // Metrics for monitoring getStats(): { total: number; expired: number; byUrgency: Record<string, number> } { const now = Date.now(); let expired = 0; const byUrgency = { critical: 0, urgent: 0, normal: 0 }; for (const item of this.items) { const remaining = item.deadline.getTime() - now; if (remaining <= 0) { expired++; } else if (remaining < 1000) { byUrgency.critical++; } else if (remaining < 5000) { byUrgency.urgent++; } else { byUrgency.normal++; } } return { total: this.items.length, expired, byUrgency }; }}Deadlines aren't just technical constraints—they're inputs to business logic. A recommendation engine with 2 seconds can do personalized ML inference; with 100ms it serves cached popular items. Both are valid responses; the deadline determines which is appropriate.
Deadline Cancellation and Cleanup
When a deadline expires, it's not enough to stop waiting—you must also clean up in-flight operations. This is especially important for operations that consume external resources or have side effects.
The cleanup imperative:
- In-flight database queries: Should be cancelled to free connection pool resources
- Pending HTTP requests: Should be aborted to prevent wasted work
- Partial transactions: Should be rolled back to maintain data consistency
- Reserved resources: Locks, rate limit tokens, and other reserves should be released
- Background work: Any spawned goroutines/tasks should be signaled to stop
Context-based cancellation pattern (Go):
Go's context.Context provides a clean model where deadline expiration automatically triggers cancellation:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
package main import ( "context" "database/sql" "errors" "log" "time") func processOrder(ctx context.Context, orderID string) error { // Context carries the deadline; all operations check it // 1. Database query - will be cancelled if deadline expires var order Order err := db.QueryRowContext(ctx, "SELECT * FROM orders WHERE id = ?", orderID, ).Scan(&order.ID, &order.Status) if err != nil { if errors.Is(err, context.DeadlineExceeded) { log.Printf("Query cancelled due to deadline") return err } return err } // 2. HTTP call - will be cancelled if deadline expires req, _ := http.NewRequestWithContext(ctx, "POST", paymentURL, body) resp, err := httpClient.Do(req) if err != nil { if errors.Is(err, context.DeadlineExceeded) { log.Printf("Payment call cancelled due to deadline") // Note: Payment might have succeeded - need idempotency handling return err } return err } defer resp.Body.Close() // 3. Long computation - periodically check context result, err := computeExpensiveResult(ctx, order) if err != nil { return err } return nil} // Long operation that respects context cancellationfunc computeExpensiveResult(ctx context.Context, order Order) (*Result, error) { result := &Result{} for i := 0; i < 1000; i++ { // Check for cancellation periodically select { case <-ctx.Done(): return nil, ctx.Err() default: } // Do some work partial := processOrderItem(order.Items[i%len(order.Items)]) result.Merge(partial) } return result, nil} // Transaction with deadline-aware rollbackfunc processOrderWithTransaction(ctx context.Context, orderID string) error { tx, err := db.BeginTx(ctx, nil) if err != nil { return err } // Defer rollback - will execute if we don't commit defer func() { if tx != nil { tx.Rollback() } }() // Execute operations within transaction if _, err := tx.ExecContext(ctx, "UPDATE inventory SET reserved = reserved + ? WHERE item_id = ?", order.Quantity, order.ItemID, ); err != nil { return err // Rollback happens via defer } // Check if deadline is approaching before committing if deadline, ok := ctx.Deadline(); ok { if time.Until(deadline) < 100*time.Millisecond { // Not enough time to safely commit return context.DeadlineExceeded } } // Commit the transaction if err := tx.Commit(); err != nil { return err } tx = nil // Prevent rollback in defer return nil}AbortController pattern (JavaScript):
In JavaScript/TypeScript, use AbortController to propagate cancellation:
12345678910111213141516171819202122232425262728293031323334353637383940414243
async function processWithDeadline( deadline: Date, operation: (signal: AbortSignal) => Promise<void>): Promise<void> { // Create abort controller const controller = new AbortController(); const { signal } = controller; // Calculate remaining time const remainingMs = Math.max(0, deadline.getTime() - Date.now()); // Set up deadline timeout const timeoutId = setTimeout(() => { controller.abort(new DeadlineExceededError('Deadline exceeded', deadline)); }, remainingMs); try { await operation(signal); } finally { clearTimeout(timeoutId); }} // Usage:await processWithDeadline(deadline, async (signal) => { // Fetch with abort signal const response = await fetch('/api/data', { signal }); // The fetch will be aborted if deadline is reached if (signal.aborted) { throw signal.reason; } const data = await response.json(); // Check periodically in loops for (const item of items) { if (signal.aborted) { throw signal.reason; } await processItem(item, signal); }});Cancelling an operation doesn't undo its effects. If a payment RPC is cancelled after the server processed it but before the response arrived, the payment still happened. Design for idempotency and implement saga patterns for multi-step operations.
Deadline Propagation Best Practices
- •Always set initial deadlines at edges — Every request entering your system should have a deadline established, either from the client or a sensible default.
- •Propagate deadlines through all calls — Whether using gRPC, HTTP, or message queues, pass deadline information to downstream services.
- •Check deadlines before expensive operations — Don't start a database transaction if you only have 100ms remaining.
- •Use buffer time for safety — When checking if there's enough time, reserve 10-20% for overhead and unexpected delays.
- •Implement graceful degradation — When time is limited, choose faster alternatives over complete failures.
- •Log deadline context — Include remaining time and deadline in logs to aid debugging.
- •Monitor deadline excess rates — Track what percentage of requests are running up against their deadlines.
- •Alert on deadline drift — If deadline-related errors spike, something in the chain is slower than expected.
- •Verify clock synchronization — Regularly check NTP status across all hosts.
- •Test deadline behavior — Write tests that verify correct behavior when deadlines are tight or expired.
- •Document deadline contracts — Make clear what deadline each service expects for its operations.
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Ignoring incoming deadlines | Downstream work may be wasted | Always check and respect deadline headers |
| Setting overly tight deadlines | Flapping behavior under slight latency | Base deadlines on P99 latency + buffer |
| No deadline on any call | Resource exhaustion from slow calls | Enforce deadlines on all outbound calls |
| Deadline longer than upstream | Work continues after caller gave up | Propagated deadline should be ≤ upstream |
| Not handling deadline cancellation | Resource leaks on deadline | Clean up resources when deadline fires |
Adopt a deadline-first mindset: before any operation, ask 'Do I have enough time?' This simple mental check prevents wasted work, resource exhaustion, and the frustration of working on already-failed requests.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.