Timeout Strategies for Long-Running Agent Steps
A timeout strategy sets hard clocks on model calls, tool calls, and whole runs so an agent cannot hang forever.
Search across all documentation pages
A timeout strategy sets hard clocks on model calls, tool calls, and whole runs so an agent cannot hang forever.
Long steps are normal in agent systems; unbounded steps are incidents waiting to happen.
Apply nested deadlines (run > turn > tool > HTTP), fail with structured timeout observations, cancel work when possible, and leave enough budget for fallbacks instead of spending the whole SLO on one hung call.
error_type=timeout) and count it toward budgets.import time
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeout
from dataclasses import dataclass
from typing import Any, Callable, TypeVar
T = TypeVar("T")
@dataclass
class Deadline:
"""Absolute deadline in time.time() seconds."""
at: float
@classmethod
def in_seconds(cls, seconds: float) -> "Deadline":
return cls(at=time.time() + seconds)
def remaining(self) -> float:
return max(0.0, self.at - time.time())
def expired(self) -> bool:
return self.remaining() <= 0
@dataclass
class Observation:
ok: bool
data: Any = None
error_type: str | None = None
message: str | None = None
def call_with_timeout(fn: Callable[[], T], timeout_s: float) -> Observation:
if timeout_s <= 0:
return Observation(ok=False, error_type="timeout", message="no budget left")
with ThreadPoolExecutor(max_workers=1) as pool:
fut = pool.submit(fn)
try:
return Observation(ok=True, data=fut.result(timeout=timeout_s))
except FuturesTimeout:
return Observation(
ok=False,
error_type="timeout",
message=f"exceeded {timeout_s:.2f}s",
)
def run_turn(tools: list[tuple[str, Callable[[], Any]]], run_deadline: Deadline) -> list[Observation]:
results: list[Observation] = []
for name, fn in tools:
# Cap each tool, but never exceed remaining run budget
tool_cap = 2.0
budget = min(tool_cap, run_deadline.remaining())
obs = call_with_timeout(fn, budget)
results.append(obs)
if not obs.ok and obs.error_type == "timeout":
# stop scheduling more tools this turn
break
if run_deadline.expired():
break
return results
def slow() -> str:
time.sleep(3)
return "late"
def fast() -> str:
return "ok"
deadline = Deadline.in_seconds(2.5)
print(run_turn([("slow", slow), ("fast", fast)], deadline))Production code should use asyncio cancel scopes, HTTP timeout= parameters, or worker job deadlines instead of only thread result timeouts.
| Layer | Typical bound | Failure mode if missing |
|---|---|---|
| HTTP / SDK call | 5-60s | Hung socket forever |
| Tool wrapper | Slightly above HTTP | Soft retries exceed UX |
| Graph node / turn | Tens of seconds | One node stalls the graph |
| Whole run | Product SLO | Zombie sessions and spend |
Each inner timeout must be less than the remaining outer budget.
Providers hang. Streams stall mid-token.
Set:
A tool timeout with an infinite model call still freezes the turn.
ThreadPool result(timeout=) alone may leave the worker thread running. Prefer APIs with real cancel.
For scrapers, browsers, or multi-page fetches:
Partial search hits beat an empty hang.
If p95 time-to-final SLO is 45s:
See Setting SLOs for Agent Latency, Success Rate, and Cost.
LangGraph and similar systems expose per-node or run-level time controls in recent versions (verify at build).
Whatever the framework:
A timeout can be retryable once with backoff if the budget allows.
Never:
On run timeout:
timed_out.| Approach | Strength | Weakness |
|---|---|---|
| Nested deadlines (this recipe) | Matches agent structure | Requires careful plumbing |
| Single run timeout only | Simple | One tool can burn the whole run |
| Async job + poll | Good for very long work | Needs product support for async UX |
| Serverless max duration only | Platform enforced | Coarse; cold starts; hard multi-step |
| Cooperative checkpoints only | Portable | Hung syscalls ignore you |
For multi-minute research, prefer async jobs with nested timeouts inside the worker.
Start from p99 of successful calls times a small factor, with a product max. Many interactive tools land in 5-20s; bulk tools need explicit higher tiers.
Often yes once, if remaining budget and error history allow. After repeated timeouts, open a breaker or fallback.
Timeouts are planned bounds on duration. Kills are emergency stops for any reason, including policy and cost.
Yes. Add idle-stream timeouts so a stalled token stream fails even if the total cap has not been hit.
Give the root a deadline and pass remaining budget into children. Children must not assume a fresh full SLO.
Pause the agent execution clock while waiting on approvals, or the run will false-timeout during business hours delays.
Allow only via host policy for named tools/tiers. Never let free-form model text raise caps unbounded.
Inject sleeps and blocked sockets in CI; assert terminal status, metrics, and that fallbacks still run when budget remains.
It helps for async code paths that honor cancellation. Confirm third-party SDKs actually cancel in-flight I/O.
Check current LangGraph docs for per-node timeout support and streaming v2 behavior (verify at build); still wrap tools with your own client deadlines.
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.
Reviewed by Chris St. John·Last updated Jul 16, 2026