
Stop Forgetting Timeouts: Prevent Cascading Failures
Discover how missing API timeouts cause cascading failures in microservices. Learn how to implement timeouts correctly in Node.js, Python, and Go.
A single missing API timeout can trigger a cascading failure that brings down your entire microservices architecture.
If you've been working in backend development or microservices long enough, you've probably experienced a failure that makes absolutely no sense at first glance. Your core API goes down, database metrics look fine, CPU isn't maxed out, but the service is completely unresponsive.
After hours of debugging, you finally trace it to a tiny, non-critical integration: a third-party analytics service or a notification API was experiencing network latency.
How did a slow external API crash your entirely separate internal service? The answer is often ridiculously simple: someone forgot to set a timeout.
Let's talk about the silent killer of distributed systems—cascading failures—and why relying on default HTTP client behaviors is a ticking time bomb waiting to destroy your production environment.
The Day the Payment System Crashed Because of an Email
A few years ago, I was working on an e-commerce platform. Everything was running smoothly until Black Friday. Suddenly, our critical Payment Service stopped accepting new requests.
My first thought was that the database was locked, or the payment gateway was down. Nope. They were both responding instantly.
Digging into the logs, I found the culprit. After a payment succeeded, our code made a synchronous API call to an external email provider to send the receipt. That email provider was under heavy load and their API didn't reject our requests—it just held the connection open, doing nothing.
Because we didn't specify a timeout, our HTTP client waited. And waited. It waited for the OS-level TCP timeout, which is typically around two whole minutes.
In a high-traffic system, holding a thread open for two minutes is a death sentence. Every time a user paid, a worker thread got stuck waiting for the email API. Within seconds, our connection pool was entirely exhausted. New checkout requests had nowhere to go, so they queued up until the Payment Service ran out of memory and crashed.
A non-critical background task completely took down our core revenue engine. This is the textbook definition of a cascading failure. And it could have been prevented with a single line of code.
The Trap of Default Behaviors

You might think, "Surely, standard HTTP libraries have sensible defaults. They wouldn't just hang forever, right?"
Wrong. Most popular HTTP clients prioritize downloading large files over failing fast. By default, they will wait almost indefinitely.
Here is what you are dealing with when you blindly use the defaults:
- Node.js
fetch: Infinite by default. It will only drop when the underlying TCP connection times out. - Axios: The default
timeoutsetting is0, which means no timeout. - Python
requests: No timeout by default. - Go
http.Client: The zero-valuehttp.Clienthas no timeout.
This is a massive footgun. When you write const response = await fetch('https://api.example.com/data'), you are essentially telling your server, "Please pause this thread forever if the remote server feels like holding the connection hostage."
How to Set Timeouts Correctly

Fixing this is easy, but you have to be intentional about it in every single network call. Let's look at how to do it in modern tech stacks.
JavaScript / TypeScript (Node.js & Browser)
In the past, adding a timeout to fetch meant writing a verbose AbortController wrapped in a setTimeout. Today, modern JavaScript gives us a much cleaner utility: AbortSignal.timeout().
// DON'T DO THIS
const data = await fetch('https://api.example.com/notify');
// DO THIS
const response = await fetch('https://api.example.com/notify', {
// Throws an AbortError if the request takes longer than 3000ms
signal: AbortSignal.timeout(3000)
});
const data = await response.json();
If you are using Axios, always explicitly set the timeout in your global instance configuration:
import axios from 'axios';
const apiClient = axios.create({
baseURL: 'https://api.example.com',
timeout: 5000 // 5 seconds. Never leave this at 0!
});
Python
The ubiquitous requests library is notorious for hanging scripts. Always pass the timeout parameter. For production systems, you can even pass a tuple to separately specify the connection timeout and the read timeout.
import requests
# DON'T DO THIS
# response = requests.get('https://api.example.com/data')
# DO THIS: (connect_timeout, read_timeout)
try:
response = requests.get('https://api.example.com/data', timeout=(3.05, 5))
data = response.json()
except requests.exceptions.Timeout:
print("The request timed out. Failing fast!")
(Pro-tip: Set the connect timeout slightly larger than a multiple of 3 seconds—like 3.05—to accommodate the standard TCP packet retransmission window.)
Go (Golang)
In Go, making an HTTP request without a timeout can leak goroutines, eventually crashing your app with an out-of-memory error.
// DON'T DO THIS
// client := &http.Client{}
// DO THIS
client := &http.Client{
Timeout: 5 * time.Second,
}
// Or even better, use Context for granular control:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/data", nil)
resp, err := client.Do(req)
What Happens After the Timeout?
Setting a timeout is only step one. It stops the bleeding, but you still have a failed request. How your application handles that failure determines its true resilience.
- Fail Fast: The most basic approach is to return a 503 Service Unavailable or a friendly error to the user immediately. It is significantly better to tell a user "Please try again later" instantly than to let them stare at a spinning loader for two minutes before showing the exact same error.
- Graceful Degradation: Can you serve the request without the external data? If a recommendation engine API times out, don't crash the homepage. Catch the timeout error and return a hardcoded list of default popular products instead.
- Asynchronous Processing: In my payment system story, we shouldn't have been calling the email API synchronously anyway. Critical paths should never block on non-critical tasks. We should have pushed an event to a message broker (like RabbitMQ or Kafka) and let a separate background worker handle the emails with its own retries and timeouts.
- Circuit Breakers: If a downstream service times out five times in a row, why send a sixth request? Implement a Circuit Breaker pattern. Once the error rate exceeds a threshold, the circuit "trips" and immediately rejects all new requests for a cooldown period. This gives the struggling downstream service time to recover without being hammered by your retries.
The Golden Rule of Distributed Systems
The network is hostile. It is not a matter of if a third-party service will slow down, but when.
If there is one piece of advice I want you to take away from this, it is to never trust the network and never trust a downstream service. Every single outbound call must be bound by a strict time limit.
Open up your codebase today. Do a global search for fetch(, axios., requests.get, or http.Client. I guarantee you will find at least one naked network call waiting to cause a production incident. Fix it, add a timeout, and sleep a little better tonight.
written by
Nguyên Tech
Responses
Loading comments…