DEV Community

Zira
Zira

Posted on

Your AI Agent Needs a Deadline Budget, Not Just a Timeout

A timeout answers one question: how long may this process wait?

An agent needs to answer a harder question: how much time is left for the whole job, and which work is still worth starting?

Without a deadline budget, a run can spend 30 seconds waiting in a queue, give every nested tool a fresh 30-second timeout, retry a provider call after the user has gone away, and finally send an obsolete result. Every individual component looks reasonable. The end-to-end contract is broken.

This article shows a small deadline contract, how to propagate it through queues and tools, and how to test the failure modes.

1. Make the deadline part of the run contract

Do not pass only timeout_seconds. Pass an absolute deadline and a stable run identity:

timeout_seconds = 30
deadline_at = now() + timeout_seconds
run_id = "run_01J..."
Enter fullscreen mode Exit fullscreen mode

An absolute deadline avoids accidentally extending a run at every hop. A child tool receives the parent deadline, not a new timeout starting at dispatch time.

A useful request envelope looks like this:

define request:
  run_id
  parent_run_id
  deadline_at
  cancellation_token
  attempt
  idempotency_key
Enter fullscreen mode Exit fullscreen mode

The envelope should be immutable after admission. If a worker needs more time, the system should create a new approved run or explicitly renew the contract. It should not silently rewrite the original deadline.

2. Reserve time for the whole path

A deadline is not only an HTTP client setting. Split the remaining time into budgets for the stages that can still produce a useful result:

  • queue wait
  • model generation
  • tool execution
  • outbound delivery
  • reconciliation and evidence writing

For example, with 30 seconds remaining, a scheduler might reserve 3 seconds for queueing, 12 seconds for the model, 10 seconds for a tool, and 5 seconds for final persistence. The numbers depend on the workload. The important part is that the reservation is explicit.

Before starting a stage, calculate:

def remaining_ms(deadline_at):
  return max(0, deadline_at - monotonic_now())

def can_start(deadline_at, minimum_required_ms):
  return remaining_ms(deadline_at) >= minimum_required_ms
Enter fullscreen mode Exit fullscreen mode

If a browser action needs at least 8 seconds and only 4 seconds remain, do not start it just because the browser client has a 30-second timeout. Return a deadline-exhausted result and preserve the unfinished work for a later, deliberate decision.

3. Propagate cancellation, not just a number

A child process must observe both the deadline and cancellation. The parent may cancel because the user disconnected, a policy changed, or a replacement run acquired the work.

In pseudocode:

def run_tool(request, tool):
  if cancelled(request) or remaining_ms(request.deadline_at) <= 0:
    return Result("NOT_STARTED", reason="deadline_or_cancel")

  child = request.with_budget(
    deadline_at=request.deadline_at,
    cancellation_token=request.cancellation_token
  )

  return tool.execute(child)
Enter fullscreen mode Exit fullscreen mode

Every adapter must translate the contract into its own client. For example:

  • HTTP: set a request deadline and cancel the request body.
  • Queue: reject admission when the remaining budget cannot cover lease and work time.
  • Browser: stop before a mutating action if the deadline is too close, and classify a timeout during the action as UNKNOWN.
  • Model streaming: stop generation and persist the partial state when cancellation arrives.
  • Child agents: inherit the parent deadline and cannot mint a later one.

A cancellation signal is not proof that a side effect did not happen. If a provider timed out after accepting a request, the result is UNKNOWN and needs reconciliation by idempotency key or provider lookup.

4. Do not retry past the deadline

Retries need a minimum viable budget, not only a maximum attempt count:

def retry_allowed(deadline_at, backoff_ms, minimum_call_ms):
  return remaining_ms(deadline_at) >= backoff_ms + minimum_call_ms
Enter fullscreen mode Exit fullscreen mode

A retry should be rejected when the backoff plus the minimum useful call cannot fit. Otherwise, a queue of expired retries can consume workers while producing no valid result.

Also separate retry classes:

  • A pure model read may be retried if the result is still useful.
  • An idempotent provider request may be retried with the same idempotency key.
  • A browser mutation or payment-like action must reconcile UNKNOWN before retrying.
  • A notification may need delivery recovery even after the agent execution has finished.

The deadline controls whether to start the retry. The side-effect policy controls whether retrying is safe.

5. Persist the reason for refusal

A useful operational record distinguishes these outcomes:

  • NOT_STARTED: admission refused because the budget was too small.
  • CANCELLED: an explicit cancellation arrived before execution.
  • EXPIRED: the deadline passed before execution completed.
  • UNKNOWN: execution may have crossed an external side-effect boundary.
  • COMPLETED: the result and evidence were durably written.

Include deadline_at, observed_at, stage, remaining_ms, and idempotency_key in the evidence. This makes it possible to tell whether a job was slow, queued too long, cancelled by a user, or incorrectly retried after expiry.

If you run an always-on OpenClaw or browser agent, the hosting layer can provide a stable runtime for these workers, but it does not define deadline semantics. You still need cancellation propagation, durable state, scoped credentials, and UNKNOWN reconciliation. If managed hosting is the part you are evaluating, managed OpenClaw hosting on Ampere is one option to compare, not a substitute for those controls.

6. Run a failure-injection matrix

A deadline contract is incomplete until the boundary cases are tested:

  1. Queue the job until 90% of its budget is gone. Verify the worker refuses admission instead of starting doomed work.
  2. Cancel while the model is streaming. Verify the stream stops and partial state is persisted.
  3. Cancel immediately before a tool mutation. Verify the mutation is not dispatched.
  4. Time out after the provider accepts the request. Verify the operation becomes UNKNOWN and is reconciled before retry.
  5. Pause a child worker and let the parent deadline expire. Verify the child cannot extend the deadline.
  6. Exhaust the budget during backoff. Verify no retry is enqueued.
  7. Fail the evidence writer after the tool returns. Verify the result is not reported as completed without an evidence record.
  8. Restart the scheduler with expired work present. Verify expired items are classified rather than blindly replayed.

Track at least these test assertions: work started after the deadline, retries after expiry, child deadlines later than parents, duplicate external effects, and completed results without durable evidence. Each should be zero.

The practical rule

A timeout protects a component. A deadline budget protects the user-visible operation.

Put one absolute deadline in the run contract. Propagate cancellation through every adapter. Reserve time before each stage. Refuse work that cannot finish usefully. Treat external timeouts as UNKNOWN when a side effect may have crossed the boundary. Then test queueing, cancellation, retries, restarts, and evidence failure as one system.

That is how an agent stops being locally well-behaved while globally too late.

Top comments (0)