Always set timeouts
I've watched production incidents unfold in real time where a single slow downstream service brought an entire platform to its knees. The common thread? No timeout was set on the outbound call. A request that should have failed in 5 seconds hung for 30 minutes, consuming connection pools, thread workers, and user patience. Always set timeouts isn't optional best practice—it's a critical defense mechanism that keeps your system responsive and prevents cascading failures.
Every outbound call gets a timeout
This is non-negotiable. HTTP requests, database queries, message queue operations, external API calls—if it leaves your process and reaches another service, it must have a timeout. The absence of a timeout means your code will wait indefinitely if the remote service hangs, becomes unresponsive, or experiences network latency.
I learned this the hard way early in my career. We had a ride-hailing platform where payment processing calls to a third-party gateway had no explicit timeout. One evening, their service degraded silently. Requests didn't fail—they just hung. Within minutes, our Node.js worker pool was exhausted, the entire booking flow stalled, and thousands of requests queued up. The fix took seconds once we added a 10-second timeout, but the incident cost us real business.
Here's what proper timeout handling looks like in NestJS with an HTTP client:
import { HttpService } from '@nestjs/axios';
import { Injectable } from '@nestjs/common';
import { timeout } from 'rxjs/operators';
@Injectable()
export class PaymentService {
constructor(private http: HttpService) {}
async processPayment(orderId: string, amount: number) {
// Set a 5-second timeout on the external payment gateway
return this.http
.post(`https://payment-gateway.com/charge`, {
orderId,
amount,
})
.pipe(
timeout(5000), // 5 seconds
)
.toPromise();
}
}
For database operations in PostgreSQL via a .NET stack, use a command timeout:
// Pseudo-code for .NET example
using (var connection = new NpgsqlConnection(connectionString))
{
await connection.OpenAsync();
using (var command = new NpgsqlCommand("SELECT * FROM orders WHERE id = @id", connection))
{
command.CommandTimeout = 10; // 10 seconds
command.Parameters.AddWithValue("@id", orderId);
using (var reader = await command.ExecuteReaderAsync())
{
// Process results
}
}
}
Even seemingly "fast" internal services need timeouts. I've seen database queries that normally run in 100ms spike to 30+ seconds under load when indexes aren't optimal or queries trigger full table scans. A timeout acts as an early warning and prevents resource exhaustion.
Timeouts shorter than the caller's timeout
This principle prevents cascading timeout failures and ensures your response bubbles up cleanly through the call stack. If Service A calls Service B with a 10-second timeout, and Service B calls Service C with a 15-second timeout, you have a problem. Service A gives up at 10 seconds, but Service B is still waiting on Service C. Those extra 5 seconds are wasted resources and unpredictable behavior.
The rule: each downstream service timeout should be shorter than its caller's timeout. This creates a timeout hierarchy that fails fast and predictably.
| Service Layer | Timeout | Reason |
|---|---|---|
| API Gateway → Service A | 30 seconds | User-facing, slower operations acceptable |
| Service A → Service B | 20 seconds | Internal, must complete before gateway timeout |
| Service B → Database | 10 seconds | Fast queries expected, fail quickly if slow |
| Service B → External API | 8 seconds | Third-party, less control, tighter bound |
In a travel booking system I worked on, we had exactly this problem. The frontend set a 60-second timeout for booking confirmation. Our booking service set 50 seconds for hotel inventory checks. But the inventory service set 60 seconds for its database queries. When queries started taking 55 seconds, the booking service would timeout correctly, but resources were left hanging on the database side, degrading other operations. We tightened the database timeout to 30 seconds, and the system stabilized.
Implement this consistently across your infrastructure. If you use connection pooling libraries, they often let you set connection-level and query-level timeouts separately—use both.
Combine with retries and circuit breakers
A timeout alone tells you something failed. Retries tell you to try again, and circuit breakers prevent repeated attempts to a failing service. These three patterns work together.
When a timeout occurs, it's often transient—a temporary spike in latency, a brief network hiccup. A single retry with exponential backoff often succeeds. But if timeouts keep happening, the circuit breaker trips, preventing further cascading calls and allowing the downstream service time to recover.
Here's a practical example using a library like Polly (in .NET thinking) or a custom retry-circuit pattern in Node.js:
import { CircuitBreaker } from 'opossum';
class InventoryClient {
private breaker: CircuitBreaker;
constructor(private http: HttpService) {
const options = {
timeout: 5000, // Circuit breaker timeout
errorThresholdPercentage: 50, // Trip after 50% errors
resetTimeout: 30000, // Retry after 30 seconds
};
this.breaker = new CircuitBreaker(
(hotelId: string) => this.checkInventory(hotelId),
options,
);
}
private async checkInventory(hotelId: string) {
return this.http
.get(`https://inventory-service.com/check/${hotelId}`)
.pipe(timeout(3000)) // Request timeout shorter than circuit timeout
.toPromise();
}
async getInventoryWithFallback(hotelId: string) {
try {
return await this.breaker.fire(hotelId);
} catch (error) {
// Circuit open or request failed—return stale cache or default
return getCachedInventory(hotelId) || { available: false };
}
}
}
The key insight: set your request timeout tighter than your circuit breaker window. If a request times out after 3 seconds, and your circuit breaker waits 5 seconds before failing the call entirely, you're waiting unnecessarily. The request timeout should fire first, the circuit breaker catches repeated failures.
In a CRM system, we combined 5-second API timeouts with 2-retry logic and a circuit breaker set to 30 seconds. When a customer data API degraded, the first request would timeout quickly. The retry would often succeed (brief blip). If it failed again, the circuit breaker would open after a few more failures, and we'd serve degraded data from a local cache instead of hammering a struggling service.
Monitor timeout rates
Setting timeouts without monitoring them is like setting speed limits without enforcement. You won't know if something is wrong until it's already impacting users.
Track three key metrics:
- Timeout count: How many requests are timing out per minute or hour. A sudden spike indicates a real problem.
- Timeout percentage: What fraction of calls to a service timeout. Even 1-2% for a critical path is concerning.
- Timeout by service: Break down timeouts by downstream dependency. This pinpoints which service is struggling.
Set alerts on thresholds. For instance, if payment processing timeouts exceed 2% in any 5-minute window, page the on-call engineer immediately. In a high-traffic e-commerce platform, even a brief spike in database query timeouts can cascade into order processing delays.
Log timeout events with context: which service, which endpoint, what was the request, how long did it actually wait?
async function callExternalService(serviceUrl: string, payload: any) {
const startTime = Date.now();
try {
const result = await fetch(serviceUrl, {
signal: AbortSignal.timeout(5000),
method: 'POST',
body: JSON.stringify(payload),
});
return result;
} catch (error) {
const elapsed = Date.now() - startTime;
if (error.name === 'AbortError') {
logger.warn('Service timeout', {
service: serviceUrl,
timeoutMs: 5000,
elapsedMs: elapsed,
payload,
timestamp: new Date().toISOString(),
});
metrics.increment('service.timeout', {
service: extractServiceName(serviceUrl),
});
}
throw error;
}
}
Use a monitoring tool like Prometheus, DataDog, or your cloud provider's native metrics. Create dashboards showing timeout trends. When you see a pattern—"database timeouts spike every morning at 9 AM"—you have actionable intelligence to optimize indexing or scaling.
Common mistakes
- Setting timeouts too long: A 60-second timeout doesn't help if your user closes the browser after 10 seconds. Set timeouts based on user expectations and SLA requirements, not arbitrary large numbers.
- Using the same timeout everywhere: A database query timeout should be tighter than an external API timeout. Context matters. Quick local operations: 1-5 seconds. Cross-datacenter: 10-20 seconds. Third-party APIs: 20-30 seconds.
- Not distinguishing connection timeout from read timeout: Connection timeout is how long you wait to establish a connection. Read timeout is how long you wait for data after connection. Both matter. A slow service might accept connections quickly but send data slowly.
- Ignoring timeouts in tests: Your integration tests should simulate timeout scenarios. Mock a slow service and verify your retry/circuit breaker logic actually works.
- Catching timeout exceptions and silently continuing: Timeouts are signals. If you catch them and continue as if nothing happened, you're hiding real problems. Handle timeouts intentionally—retry, fail fast, return stale data, or alert monitoring.
Key takeaways
- Every outbound call—HTTP, database, message queue—must have an explicit timeout. No timeout means indefinite waiting and resource exhaustion.
- Structure timeouts hierarchically: downstream timeouts shorter than upstream. This ensures failures bubble up cleanly without resource leaks.
- Combine timeouts with retries and circuit breakers. Timeouts detect problems, retries handle transient failures, circuit breakers prevent cascading failures.
- Monitor timeout rates by service and set alerts on thresholds. Timeouts without visibility are useless—you won't know when things break.
- Choose timeout values based on realistic latency and user expectations, not arbitrary defaults. Different services need different bounds.






Comments
Loading comments…