Hey curious engineers, welcome to the twenty third issue of The Main Thread.

Wrong timeout settings cause more outages than bugs.

I know that sounds dramatic. But think about it: your code can be perfect, your logic flawless, your tests detailed, and your system still fails because someone has set a timeout to 30 seconds when it should have been 3.

In my career, I have seen a 5-minute outage turn into a two hour cascading failure because retry settings amplified a small problem into a catastrophe. As an on-call engineer, I have felt the pain directly by looking at dashboards showing “everything is timing out“ with no idea which timeout is causing all of this. I have also debugged incidents where changing a single number in the config file fixed the issue.

To be honest, this stuff is not glamorous and it doesn’t get conference talks, yet it is the difference between a robust and a flaky system.

Let’s fix that.

Four Timeout Types

When someone says “the timeout is 10 seconds“, they could mean 4 different things. Each has different failure modes, and confusing them causes real problems.

1. Connection Timeout

This is how long you wait to establish a connection (TCP handshake). If the server is down, overloaded, or unreachable, you will encounter this timeout.

# (connect_timeout, read_timeout)
requests.get(url, timeout=(3.0, 10.0))  
# The first value is connection timeout

Typical value is 1-5 seconds. Connection establishment should be fast. If the server can’t accept your connection in 3 seconds, something is seriously long.

A common mistake is setting this too high. A 30-second connection timeout means your thread sits blocked for 30 seconds before finding that the server is unreachable. If you multiply this by hundreds of requests, you will exhaust your thread pool.

2. Read Timeout

This is how long you wait for the server to send data once the connection is established. This covers the time from “request sent“ to the “first byte received“ and between subsequent chunks.

# The second value is read timeout
requests.get(url, timeout=(3.0, 10.0))

It’s typical values depend on what you are calling. A cache lookup should respond in 50ms. A complex DB query might need 5s. An ML inference call might need 30 seconds.

A common mistake I have seen engineers make is using the same read timeout for everything. Your cache client and your report-generation service have wildly different performance characteristics. You must treat them differently.

3. Write Timeout

This timeout explains how long to wait while sending data to the server. It is less common but comes into picture for large uploads or slow networks.

Naturally, it’s proportional to the size of the payload. A small JSON body doesn’t need much time but a large file upload needs significantly more.

4. Overall Deadline

This is the total time allowed for an entire operation, including retries. This is the most important timeout and the most commonly forgotten.

# Without deadline: each retry gets full timeout
# 3 retries × 10s timeout = 30s total (worst case)

# With deadline: total operation time is bounded
deadline = time.time() + 5.0  # 5 seconds from now
while time.time() < deadline:
    try:
        return make_request(timeout=min(2.0, deadline - time.time()))
    except TimeoutError:
        continue
raise DeadlineExceeded()

A common mistake is configuring per-request timeout without an overall deadline. Your users don’t give a damn that each retry was 10 seconds, they give a damn when they waited for 40 seconds before seeing an error.

Anatomy of a Timeout Incident

Let me walk you through timeout misconfiguration causes real outages.

Setup

Service A calls Service B, which calls Service C. Each service has 30-second timeout configured. Service C is actually a DB that normally responds in 50ms.

Trigger

Service C gets slow. A bad query plan, disk pressure, whatever. Response times go from 50ms to 35 seconds.

What Happens

  1. Service C takes 35 seconds to responds (exceeds 30 seconds timeout)

  2. Service B times out, returns error to Service A

  3. Service A’s request takes 30+ seconds, exhausting its thread pool

  4. Service A cannot accept new request: it’s now “down“

  5. Users see Service A as the problem

  6. On-call engineer debugs Service A, finds nothing wrong with its code

  7. 45 minutes later, someone checks Service C.

Cascade

  • Service C was slow: 1 service affected

  • Service B and A had long timeouts: 2 more services affected

  • These long timeouts caused thread pool exhaustion and users faced outage

  • On-call engineer invested wrong service first: this extended incident duration

Fix

Service A’s timeout to Service B should be 3 seconds, not 30. If the response is not coming in 3 seconds, fail fast and show errors to the users. It is foolish to let one slow dependency take down everything damn thing.

Exponential Backoff With Jitter

When a request fails, the instinct is to retry immediately. This is almost always wrong.

Subscribe to keep reading

This content is free, but you must be subscribed to The Main Thread to continue reading.

Already a subscriber?Sign in.Not now

Reply

Avatar

or to participate

Keep Reading