Failures
Failures
GitHub
Home Docs Timeout / Ambiguous Outcome
CRITICAL

Timeout / Ambiguous Outcome

"Timeout does not equal failure. Did it succeed?"

Invariant: System must handle ambiguous outcomes without duplicate side effects.

Applies To

external APIs, payment providers, databases, queues

Why It Happens

A first-principles walkthrough of why a timeout tells you nothing about success. TCP hid whether the bytes arrived, HTTP hid whether the handler ran. Every timeout is ambiguous by design, so we never treat it as failure: persist pending before calling, then reconcile with the provider as the source of truth, never blindly retry a non-idempotent charge.

How It Works Underneath

Timeout is a client-side decision to stop waiting. It tells you nothing about the server: the server may have succeeded a millisecond after you gave up, may be still running, or may never have received the request. This is the ambiguous outcome — the most dangerous failure class. TCP already hid whether the bytes arrived; HTTP hides whether the handler committed. Treating timeout as failure and blindly retrying a non-idempotent charge is how double debits happen.

The correct machinery is: never retry without an idempotency key, persist a pending state before calling, and on timeout do not guess — reconcile. The reconciler asks the provider “what is the truth for reference X?” and drives the local row to completed or failed. The provider, not the timeout, is the source of truth.

try: POST /charge Idempotency-Key: op_123 timeout=5s → Timeout
not: retry POST → instead: GET /charge/op_123 (reconcile) → if confirmed return, else mark pending for sweeper

Common Misconceptions

“Timeout means it failed.” No — it means “I don’t know.” Another: “A longer timeout fixes it.” A longer wait just moves the ambiguity window, it does not remove it.

How to Detect It

Every outgoing HTTP call must have an explicit timeout=. Search for requests.post or httpx without timeout — that worker will hang forever and its pool will exhaust.

Cataloged Failure Modes

Code Comparison

Language:
Fragile (AI Happy Path) — Python timeout_fragile.py
# NAIVE: Treating timeout as failure
async def pay_invoice(invoice_id: str, amount: float):
    try: return await http_client.post("/charge", json={"amount": amount}, timeout=10.0)
    except TimeoutException: return await http_client.post("/charge", json={"amount": amount})
Resilient (Failures Verified) — Python
timeout_safe.py
# IMPROVED: Handling timeout as AMBIGUOUS
async def pay_invoice(invoice_id: str, amount: float):
    op_id = f"inv_{invoice_id}_{uuid4()}"
    try:
        res = await http_client.post("/charge", json={"amount": amount}, headers={"Idempotency-Key": op_id}, timeout=10.0)
        return res.json()
    except (TimeoutException, httpx.NetworkError):
        status = await reconcile_with_provider(op_id)
        if status.is_confirmed: return status.data
        raise OperationPendingException("Payment pending confirmation")

Mitigation Patterns