One 300-Second Timeout Tells You Nothing. Agents Need a Deadline Budget.

A run that dies at its ceiling with no attribution tells you nothing about which part was slow — and pushes you toward the laziest fix, raising the ceiling. Three nested clocks, an inherited remainder, and a reserved tail slice turn timeouts into a signal you can actually read.

One 300-Second Timeout Tells You Nothing. Agents Need a Deadline Budget.

A nightly log-triage agent gets killed at the 300-second mark. The alert says run timeout exceeded. That is the entire diagnostic. Was one API call hanging for four minutes, or did the agent make forty perfectly healthy steps and simply need more room? A single run-level timeout cannot distinguish those, and they need opposite fixes.

A run timeout is a circuit breaker, not an instrument. It stops the bleeding and destroys the evidence. What production agents actually need is a deadline budget: several clocks at different scopes, each with its own limit, each logging which one fired.

⚙️ One timeout collapses three different failures

When a run hits its ceiling, exactly one of these is usually true:

  • A single call is hanging. A vendor API is degraded and holding the socket open. The agent made three steps and then stopped existing.
  • One step is thrashing. The model is generating a long chain of output, or an internal retry loop is quietly burning attempts on a malformed tool response.
  • The run is genuinely long. Every step is fast and correct; there are just more of them than expected because the input was bigger than usual.

The remediation for the first is a per-call deadline and a circuit breaker on that dependency. For the second it is an output cap and a retry limit. For the third it is a larger budget, or a scope reduction, or nothing at all — the agent was working fine. A single number treats all three as the same incident and pushes you toward the laziest response, which is raising the ceiling until the pager stops.

The three clocks

Give the run a budget object and pass it down through every layer.

class Budget:
    def __init__(self, wall_seconds, max_steps, max_tokens):
        self.deadline   = time.monotonic() + wall_seconds
        self.steps_left = max_steps
        self.tokens_left = max_tokens

    def remaining(self):
        return self.deadline - time.monotonic()

    def check(self):
        if self.remaining() <= 0:  raise Expired("run_wall_clock")
        if self.steps_left <= 0:   raise Expired("run_step_cap")
        if self.tokens_left <= 0:  raise Expired("run_token_cap")

Clock 1 — the run. Wall clock, plus a step cap, plus a token cap. Three limits, but one scope: the whole run. The step cap catches loops that are individually fast; the token cap catches a run that is cheap in time and expensive in money. A wall clock alone misses both.

Clock 2 — the step. One model call plus its tool call plus parsing. This is the clock that catches thrash. Set it from the observed latency of a normal step, not from the run budget divided by expected steps — those diverge the moment step counts vary.

Clock 3 — the call. Each individual tool invocation gets its own deadline, sized to that specific dependency. A vector search and a report-generation endpoint do not deserve the same number.

The rule that makes it work: children inherit the remainder

This is the part most implementations skip, and it is what turns three clocks from bookkeeping into a real mechanism. A tool's default timeout is a ceiling, never a floor:

def call_tool(name, args, budget):
    reserve = 0.10 * budget.total          # tail time, see below
    allowed = budget.remaining() - reserve
    timeout = min(TOOL_DEFAULTS[name], allowed)
    if timeout <= MIN_USEFUL:
        raise Expired("insufficient_budget", tool=name)
    ...

Without that min, a tool with a 60-second default will happily start a 60-second wait when 12 seconds of run budget remain. The call cannot possibly finish in time, so its result is thrown away — you paid full latency for a guaranteed discard. Checking the remainder before starting turns a wasted minute into an immediate, explainable stop.

Reserve tail time so the run can land

A killed run returns nothing. A run that notices it is nearly out of budget can return what it has.

Hold back a slice of the wall clock — a tenth is a reasonable starting point — and when the working budget is exhausted, spend the reserve on one final, tool-free model call: summarize findings so far, list what was not checked, mark the result partial. For the triage agent, that is the difference between an empty alert and "scanned 6 of 9 services, two anomalies found in auth, billing and search unchecked."

Partial output also makes the next run cheaper, because it tells you where to resume.

🔍 Log which clock fired

Every expiry event should carry:

  • ✅ the name of the limit that tripped (run_wall_clock, step_deadline, tool:invoice_api)
  • ✅ remaining budget at each scope when it tripped
  • ✅ step index and the tool in flight
  • ❌ not just TimeoutError with a stack trace

With that field in place, "our agents time out sometimes" becomes a distribution you can read. If 80% of expiries are tool:invoice_api, the fix is a dependency problem and no amount of raising the run ceiling will help. If they are all run_step_cap on large inputs, the agent needs decomposition, not patience.

Picking the numbers

A workable starting method, no benchmarks required:

  1. Set each tool deadline near the observed p95 latency of that dependency, rounded up. Not p99 — p99 is often the hang you want to cut.
  2. Set the step deadline to roughly the slowest normal step, doubled.
  3. Set the step cap to about twice the step count of a typical successful run. Runs beyond that are usually looping, not working.
  4. Set the run wall clock last, from what the caller can actually wait for — a synchronous request and a nightly batch job have completely different answers.
  5. Reserve the tail slice from the run budget, not from the step budget.

Then watch which clock fires most often and adjust that one. One clock firing 90% of the time means it is the only limit doing work, and the others are decoration.

A run that dies at its ceiling with no attribution is not a timeout — it is an unexplained disappearance. Budget the deadline, pass the remainder down, keep a tail slice, and name the clock that stopped you.

Keep reading

Similar posts

Matched on shared tags and category — the more bars, the stronger the overlap with what you just read.