← Home ← Codex ← DEBT
Browse by Category
+ added · updated 7d
← Back to glossary

API Request Timeout Handling

API Design Intermediate
debt(d7/e5/b5/t7)
d7 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'only careful code review or runtime testing' (d7). The code_pattern regex and phpstan/semgrep can flag a missing-timeout client construction, but the deeper problems — retrying non-idempotent writes, missing backoff, deadline propagation — are not reliably caught by static tools and stay silent until a slow upstream causes an outage in production. The non-automated flag (automated: no) pushes this toward d7 rather than d5.

e5 Effort Remediation debt — work required to fix once spotted

Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix of setting explicit timeouts is locally a one-liner, but doing it correctly across every outbound call, adding backoff/jitter, capped retries, circuit breakers, and shared deadline propagation spans many call sites and shared HTTP client config — a significant refactor, not a single-line swap.

b5 Burden Structural debt — long-term weight of choosing wrong

Closest to 'persistent productivity tax' (b5). It applies_to web, cli, queue-worker, library and microservice contexts, so resilience policy is load-bearing across every outbound integration. A shared deadline/retry strategy shapes many work streams without quite defining the whole system's architecture.

t7 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'serious trap' (t7). The misconception — that a retry on failure always makes the call more reliable — is the canonical wrong belief; retrying a non-idempotent write after a timeout can duplicate charges, and retrying without backoff amplifies load on a struggling server. The obvious instinct (just retry harder) actively makes things worse, contradicting expected behaviour.

About DEBT scoring →

Also Known As

request timeouts retry and backoff circuit breaker client resilience

TL;DR

Client-side deadlines, retries with backoff, and circuit breakers that keep your app responsive when an upstream API fails to reply in time.

Explanation

Request timeout handling is the client-side discipline of bounding how long you wait for an upstream API and deciding what to do when that bound is exceeded. The single most common production incident with HTTP clients is an unbounded request that hangs forever, exhausting connection pools, threads, or worker processes until the whole service falls over. Every outbound call needs an explicit connection timeout (time to establish the TCP/TLS handshake) and a read/response timeout (time to receive the response body). A total deadline that spans retries is even better, so a slow dependency cannot blow your own SLA.

When a timeout fires you have a decision to make. For idempotent operations (GET, PUT, DELETE, or POST guarded by an idempotency key) you can retry, but only with exponential backoff and jitter so that a recovering server is not hammered by a synchronised retry storm. Blind retries on non-idempotent writes can double-charge a customer or duplicate an order. Cap the number of attempts and the total elapsed time, and propagate a remaining-budget deadline so nested calls do not each restart the clock.

A circuit breaker adds a layer above retries: after a threshold of failures it trips open and fails fast for a cooldown window instead of letting every request wait out the full timeout. This protects both the caller (no thread pile-up) and the struggling dependency (no thundering herd). After the cooldown it allows a trial request (half-open) before closing again.

Good timeout handling also means surfacing a meaningful error to the caller (504 Gateway Timeout or a degraded fallback) rather than a generic 500, and emitting metrics so you can see timeout rates climb before they cause an outage. The goal is graceful degradation: a slow dependency should make one feature slow, not take the entire application down.

Common Misconception

A retry on failure always makes the call more reliable. Retrying a non-idempotent write after a timeout can duplicate the operation, and retrying without backoff amplifies load on an already-struggling server.

Why It Matters

An outbound HTTP call without a timeout will eventually hang and exhaust your connection pool or worker processes, turning one slow dependency into a full outage. Bounded deadlines, backoff, and circuit breakers contain the blast radius.

Common Mistakes

  • Making HTTP calls with no explicit connection or read timeout, so a stalled upstream hangs the request indefinitely.
  • Retrying non-idempotent POST requests after a timeout, causing duplicate charges or duplicate records.
  • Retrying immediately without exponential backoff and jitter, creating a synchronised retry storm against a recovering server.
  • Restarting the full timeout budget on each nested call so a chain of three calls can take 3x your intended deadline.
  • Catching the timeout and returning a generic 500 instead of a 504 or a degraded fallback the caller can act on.

Avoid When

  • Do not add automatic retries to non-idempotent writes unless they are protected by an idempotency key or server-side deduplication.
  • Avoid aggressive low timeouts on legitimately long operations (large uploads, report generation) - use a higher per-route budget or async processing instead.
  • Skip a circuit breaker for a dependency that is never on a hot path and has no fan-out impact - the added complexity is not worth it.

When To Use

  • Apply explicit connect and read timeouts to every outbound API call, with no exceptions.
  • Use exponential backoff with jitter and a capped attempt count whenever you retry idempotent requests.
  • Add a circuit breaker for high-traffic dependencies where a slow upstream could exhaust your connection pool or worker threads.
  • Propagate a shared remaining-time deadline across a chain of nested service calls so the whole request respects one budget.

Code Examples

✗ Vulnerable
<?php
use GuzzleHttp\Client;

// No timeouts: this call can hang forever and pin a worker.
$client = new Client();

function chargeCustomer(Client $client, array $payload): array
{
    // Naive retry loop on a non-idempotent POST.
    for ($i = 0; $i < 3; $i++) {
        try {
            $res = $client->post('https://payments.example.com/charges', [
                'json' => $payload,
            ]);
            return json_decode((string) $res->getBody(), true);
        } catch (\Exception $e) {
            // Immediate retry, no backoff -> retry storm.
            // And we may have already charged the customer once.
        }
    }
    throw new \RuntimeException('charge failed');
}
✓ Fixed
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\ServerException;

$client = new Client([
    'connect_timeout' => 2.0,  // TCP/TLS handshake budget
    'timeout'         => 5.0,  // total response budget
]);

function chargeCustomer(Client $client, array $payload, string $idempotencyKey): array
{
    $maxAttempts = 3;
    $base = 0.2; // seconds

    for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
        try {
            $res = $client->post('https://payments.example.com/charges', [
                // Idempotency key makes the retry safe: server dedupes.
                'headers' => ['Idempotency-Key' => $idempotencyKey],
                'json'    => $payload,
            ]);
            return json_decode((string) $res->getBody(), true);
        } catch (ConnectException | ServerException $e) {
            if ($attempt === $maxAttempts) {
                throw $e; // surface as 504 upstream
            }
            // Exponential backoff with full jitter (seconds).
            $ceiling = min($base * (2 ** ($attempt - 1)), 2.0);
            $sleep = mt_rand(0, (int) ($ceiling * 1000)) / 1000.0;
            usleep((int) ($sleep * 1_000_000));
        }
    }
    throw new \RuntimeException('charge failed');
}

Added 12 Jun 2026
Views 7
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 2 pings F 2 pings S 0 pings S 1 ping M 0 pings T 0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W
No pings yet today
No pings yesterday
Google 3 Perplexity 1 Ahrefs 1
crawler 5
DEV INTEL Tools & Severity
🟠 High ⚙ Fix effort: Medium
⚡ Quick Fix
Set explicit connect and read timeouts on every HTTP client, and only retry idempotent calls using exponential backoff with jitter and a capped attempt count.
📦 Applies To
any web cli queue-worker library microservice laravel symfony
🔗 Prerequisites
🔍 Detection Hints
new\s+Client\(\s*\)|new\s+Client\(\s*\[(?![^\]]*timeout)
Auto-detectable: ✗ No phpstan semgrep
⚠ Related Problems
🤖 AI Agent
Confidence: Medium False Positives: Medium ✗ Manual fix Fix: Medium Context: Function Tests: Update


✓ schema.org compliant