7 min read

External call budgets that stop agent loops

External call budgets set request, time, and spend limits that stop autonomous agents from retrying, polling, and buying without control.

External call budgets that stop agent loops

Autonomous agents do not need unlimited access to outside services to be useful. They need a defined amount of permission to try, wait, retry, and spend before they hand the work back. Without that boundary, a minor ambiguity such as an empty search result or a delayed job can turn into hundreds of requests, duplicate side effects, and a bill nobody expected.

An external call budget is a runtime contract for a single agent run. It caps request attempts, elapsed time, and monetary exposure, then forces a visible stop when any cap runs out. Treat it as an operating limit, not a suggestion in the agent prompt. Prompts are easy to reinterpret when the model is trying to finish a task; enforcement must sit outside the model.

A budget must count attempts, time, and money separately

A single limit cannot cover the ways an agent burns resources. Request count catches loops. An elapsed-time limit catches slow polling and retry sleeps. A spend limit catches a small number of expensive actions. Each measures a different failure mode, and each can end the run even while the other two remain available.

Count attempts, not successful responses. An attempted request that times out may still have reached the provider. It consumed a socket, a provider's work, and often an entry in a metered API. If you count only successes, an agent can make limitless failing requests while it searches for a response it likes.

Track at least these values for every run:

  • attempts_used and attempts_remaining, including retries and redirects that issue a fresh external request
  • deadline_at, using a monotonic clock for elapsed duration rather than wall time
  • reserved_spend and settled_spend, in the provider's smallest practical billing unit
  • side_effect_attempts, a separate count for actions that write, send, create, or purchase
  • budget_stop_reason, which records the first limit that denied further work

Do not merge request count with spend by assigning every request an invented price. A metadata lookup and a model generation endpoint may both cost one request while their invoices differ wildly. Conversely, an API may report no price for a request that still creates a chargeable cloud resource. Use a request ceiling even when a cost estimate exists.

A useful initial budget for an agent that gathers data from a known set of APIs might allow 40 total attempts, 10 minutes of runtime, and a small fixed reservation. A deployment agent may need fewer requests but a stricter side-effect allowance. A research agent that follows discovered URLs needs a much smaller reachable scope than its author will first propose. These are starting hypotheses, not universal settings. Measure ordinary runs, then set limits close enough to interrupt abnormal behavior before it turns into a cleanup job.

The budget belongs to a run, not to the model process for the day. If an agent receives a new task, it gets a new run identifier and a new, explicit allocation. A shared daily counter hides the expensive run among normal work. It also lets one task consume capacity intended for another.

The allowance must be reserved before the first costly call

Spend caps fail when a system learns the price only after it has bought the thing. Reserve the maximum plausible charge before the call, reduce the remaining budget immediately, and reconcile later when the provider returns actual usage. If the reservation cannot fit, deny the call before it leaves your boundary.

Consider an agent that may create a hosted build. The provider's request accepts a machine class, region, timeout, and artifact size, but the exact charge appears after completion. The agent's gateway needs a price table or a conservative estimate. It does not need perfect accounting to make a safe decision.

A reservation record can look like this:

{
  "run_id": "run_7c1f",
  "budget": {
    "attempts_remaining": 18,
    "deadline_at": "2025-04-21T14:42:00Z",
    "spend_remaining_cents": 1200
  },
  "reservation": {
    "action": "create_build",
    "maximum_cents": 850,
    "provider_reference": "build-request-41"
  },
  "decision": "allow"
}

After that reservation, the run has 350 cents left for every other action. If the actual bill settles at 620 cents, release 230 cents. If it settles at 910 cents, record the overage, deny later spend, and investigate why the estimate missed. Do not let an agent borrow freely from a future budget because the final number was inconvenient.

Some providers expose a price in the request or return a usage estimate before work begins. Use it. Some do not. For those services, classify actions by risk. Allow a cheap read under a normal request budget. Require explicit approval for an action that can create an uncapped resource, send paid messages, place an order, or trigger work whose cost you cannot bound.

Teams often reject reservations because estimates are imperfect. That argument confuses accounting precision with control. A fire door does not need to calculate the exact heat of the fire before it closes. Conservative reservation may reject a task that would have fit, but an agent can ask for a larger allocation with context. It is better than discovering an uncapped charge after the agent has vanished.

Keep provider credits and organization-wide quotas outside the run budget. They are backstops, not a substitute. A monthly quota usually permits a single run to consume far more than its task deserves.

Retries need a smaller allowance than first attempts

Retries should be rare, bounded, and classified by whether repeating the request can repeat the side effect. A generic instruction to retry errors is how agents turn a temporary outage into an expensive traffic pattern.

RFC 9110 defines idempotent methods such as GET, PUT, and DELETE in terms of intended server effect. It also says a client should not automatically retry a non-idempotent request unless it knows that repeating it is safe. That caveat matters more with agents than with ordinary application code: the agent may alter its payload between attempts, decide an earlier response was incomplete, and issue what looks like a fresh request.

RFC 6585 defines HTTP 429, Too Many Requests, and says a response may include Retry-After. Respect that header when it is present. Do not treat it as permission to sleep until the header expires and then resume forever. The retry still consumes the run's time budget, and the original task may no longer justify waiting.

Use a retry ledger that answers four questions before every repeat: what failed, whether the remote action may have occurred, how long to wait, and which budget will pay for another attempt. A practical policy looks like this:

request_classes:
  read:
    max_attempts: 3
    retry_on: [408, 429, 502, 503, 504]
    backoff_seconds: [2, 8]
  idempotent_write:
    max_attempts: 2
    require_idempotency_token: true
    retry_on: [408, 429, 503]
  non_idempotent_write:
    max_attempts: 1
    retry_on: []

This policy prevents a familiar failure. An agent sends POST /invoices and loses the connection before it receives a response. A careless retry may create a second invoice. An idempotency token lets a cooperative provider recognize the duplicate, but only if the agent reuses the same token and exactly the same logical operation. If the agent changes the amount, customer, or token during its second attempt, the protection no longer applies.

For a non-idempotent action, prefer a status lookup using a client-generated operation identifier. If the provider cannot support idempotency or status retrieval, treat an ambiguous timeout as requiring human review. That feels slower than an automatic retry until someone has to reverse duplicate transfers, orders, or messages.

Jitter matters when many agents receive the same outage. Use randomized backoff so they do not all retry at once. Keep the schedule short enough that the run deadline remains meaningful. A six-hour exponential backoff may protect the provider while keeping an agent session open long after the task has ceased to matter.

Polling loops need their own ceiling

An agent waiting on asynchronous work should receive an explicit poll allowance because normal request limits hide the pattern until it is already costly. Polling has a legitimate use, but it must have an interval, a maximum number of checks, and a deadline that belongs to the operation.

The bad version is easy to recognize in logs:

14:00:03 POST /exports                 202 accepted
14:00:04 GET  /exports/ea91            202 running
14:00:05 GET  /exports/ea91            202 running
14:00:06 GET  /exports/ea91            202 running
...
14:11:58 GET  /exports/ea91            202 running

The agent has interpreted “still running” as an instruction to ask again. That is not persistence. It is an absent policy.

Start from the provider's documented guidance. If an endpoint returns Retry-After, honor it within the remaining deadline. If it returns an estimated completion time, do not poll before that point. If it offers a webhook or callback, use that instead of holding an agent run open. An external callback can resume a controlled workflow later; it should not resurrect an expired run with its old authority.

A poll budget should also distinguish job state from transport failure. 202 running tells you the job exists. A timeout tells you nothing about whether your status request arrived. Do not spend the same retry allowance on both cases. The first can wait until the next scheduled check. The second may justify one retry, subject to the ordinary retry limit.

Set a terminal action for an expired poll allowance: record the last known job state, preserve the remote operation identifier, and return a resume instruction. Do not cancel automatically unless the original task said cancellation is safe. Some jobs leave a usable result after the agent gives up waiting, and some cancellation requests have their own side effects.

This design makes delayed work visible to a person. “Export still running after six checks; operation ea91 can be checked later” is a useful handoff. “Agent completed” after a hidden background loop is not.

A deadline must cover waits, tools, and queue time

See who requests authority
A new agent process shows its code-signing authority before its session receives approval.

A run deadline should measure the whole period during which the agent can cause external work. It must include model deliberation between calls, retry sleep, local tool execution, queue delay, DNS stalls, and time spent waiting for an approval. A timer around the HTTP client alone leaves large gaps where a loop can continue.

Use a monotonic elapsed timer to enforce this limit. Wall clocks can jump when a laptop sleeps, resumes, or receives a time correction. Store wall-clock timestamps for audit records, but calculate expiry from an elapsed duration that does not move backward.

Pass the remaining time down to every operation. If a run has 40 seconds left, it should not start an HTTP request with a 90-second client timeout or invoke an SSH command with no timeout at all. The child operation gets the smaller of its local cap and the run's remaining duration.

remaining = run_deadline_monotonic - now_monotonic
if remaining <= 0:
    deny("run_deadline_exhausted")
else:
    operation_timeout = min(remaining, endpoint_timeout)
    execute(operation_timeout)

Treat approval waits carefully. A person may return in five minutes, but the agent's run may only have 30 seconds left. When the approval arrives after expiry, deny the action and show why. Allowing the approval to revive the expired run creates a loophole: the agent can queue an arbitrary number of expensive actions before its deadline, then execute them later as someone clicks through cards.

Long tasks need a different shape. Split them into checkpoints with fresh budgets and a recorded state transition. For example, an agent can submit an export under one run, then a later run can inspect the completed export and decide what to do with it. Each run receives a purpose, a small authority set, and a clear end time. This is less magical than one immortal agent session, which is exactly why it is easier to audit.

Scope limits stop the agent from spending its budget in the wrong place

A budget answers how much external activity a run may perform. Scope answers where and what it may touch. Without scope, an agent can spend a perfectly reasonable request allowance probing unrelated hosts, following a poisoned link, or selecting a costly endpoint that was never part of the task.

Define scope in concrete terms: approved hosts, credential identities, HTTP methods, SSH destinations, allowed paths or command families, and maximum request body size. Keep the definition narrow enough that a reviewer can understand it. Do not try to write a miniature programming language for every possible condition. Complex policy systems accumulate exceptions until nobody can predict their result under pressure.

The distinction between a credential boundary and a budget boundary matters. A credential boundary decides whether an agent may use a secret to call a service. A budget boundary decides whether this run may make another call after it has that permission. Teams often install the first and assume it provides the second. It does not. A perfectly protected API token can still fund a thousand unnecessary requests.

For HTTP, reject redirects that leave the approved host set unless a person explicitly allowed the destination. Redirect handling is easy to overlook because many clients follow redirects automatically. A request that begins at an approved URL can end at a different host, carrying headers or consuming a budget on a service the task never named.

For SSH, avoid giving an agent a general shell merely because its first job involves one command. Restrict the destination and command interface where possible. Set a command timeout and count each connection attempt. A failed connection loop is still external activity, even if it never authenticates.

Separate read scopes from write scopes. A data collection task may need a broad set of read endpoints and no ability to create records. A change task may need one write endpoint and a narrow preflight read. This division makes the budget more meaningful because a request count cannot distinguish harmless repetition from repeated side effects on its own.

Denials must leave enough evidence to reconstruct the loop

Stop external actions at once
Lock the vault and every action is denied until you unlock it with the hardware-gated vault gate.

A budget stop is only useful if you can explain it after the fact. Record each decision before the external call begins, record the outcome after it finishes, and record denials with the same care as successful calls. Otherwise a missing event can look identical to a blocked request.

Use one immutable run identifier across model messages, local tools, HTTP requests, SSH connections, approvals, and audit records. Each external attempt should include a sequence number. If a retry happens, link it to the original logical operation and state whether the action was idempotent, ambiguous, or confirmed absent.

A compact event record contains enough to investigate without storing sensitive payloads:

{
  "run_id": "run_7c1f",
  "attempt": 17,
  "logical_operation": "fetch_export_status:ea91",
  "channel": "http",
  "destination_class": "approved-export-api",
  "outcome": "denied",
  "reason": "poll_allowance_exhausted",
  "attempts_remaining": 0,
  "elapsed_ms": 598244,
  "reserved_spend_cents": 0
}

Do not log bearer tokens, passwords, private keys, or complete request bodies by default. A budget audit needs identifiers, classes, hashes where useful, and decision context. It does not need a second secret store full of the same credentials you tried to protect.

Make the event sequence tamper-evident. A hash chain gives every record a dependency on the one before it, so removal or alteration breaks verification. Store verification material separately from the live agent where practical. An agent that can write its own history must not be able to edit the record that proves it exceeded the budget.

Sallyport records both agent sessions and individual calls from a write-blind encrypted, hash-chained audit log, and its sp audit verify command can verify the chain offline over ciphertext. That design is useful for budget enforcement because a denied action remains part of the evidence rather than disappearing as an internal decision.

Human approval belongs on exceptions, not on every read

Keep the run and call history
Sessions journal records agent runs, while Activity journal records the individual HTTP and SSH calls.

Human approval should handle changes in consequence or scope that a numeric budget cannot judge. It should not become a rubber stamp for routine calls. If a person sees an approval card for every request, they will approve the tenth prompt with the same attention they gave the first.

Ask for approval when an action creates a new payee, spends beyond the reservation, writes to a production system, sends externally visible communication, changes the approved destination set, or resumes after a material change in task input. The approval should show the action in plain language, the destination, the budget impact, and the identity of the process requesting it.

Do not frame approval as a cure for a weak budget. A distracted person can authorize a bad action, and an unattended run may never receive a response. The budget still needs to expire while the approval waits. A person can grant a fresh allocation for a new run if the task remains worth doing.

Avoid asking people to approve “continue” without context. Continue what? Against which service? At what estimated charge? How many calls has the run already made? A good approval request makes those facts visible. A vague request pushes the reviewer to trust the agent's summary, which is the exact party that has an incentive to keep working.

There is a useful division of labor. The gateway enforces fixed limits consistently. The person decides whether an exceptional action has enough value to justify more authority. Keep those responsibilities separate, and neither must pretend to solve the other's job.

Budget exhaustion should produce a resumable handoff

When a limit stops a run, the agent should return a short state record that lets a person or later run continue safely. A vague failure message invites someone to rerun the entire task, which repeats completed calls and may repeat side effects.

The handoff needs the task objective, completed operation identifiers, pending remote work, the exact stop reason, and a recommendation for the next allocation. It should say whether retrying is safe. It should never conceal that an action ended in an ambiguous state.

For example:

Stopped: elapsed-time budget exhausted.
Completed: submitted export job ea91; downloaded no files.
Last observed state: running at 14:09:58 UTC.
External attempts: 14 of 14; spend reserved: 0 cents.
Safe resume: check status of ea91 once after 14:20 UTC.
Unsafe action: do not submit another export request.

That message prevents the most expensive form of recovery: starting over because nobody knows what happened. It also makes budget tuning possible. If many ordinary runs stop after one status check, the allowed duration is too short. If runs commonly spend their full allowance on searches with no progress, the task decomposition or scope is wrong.

Do not automatically replenish a budget after exhaustion. A replenishment rule is merely an unlimited budget written in smaller increments unless a separate authority decides when it applies. Require a new run or an explicit human action, preserve the old record, and let the next attempt state why it deserves more room.

The first policy worth enforcing is simple: every autonomous run gets a finite request count, a hard deadline, and a spend reservation before it reaches an external service. Add scope restrictions and good records immediately after. Once an agent has learned that it cannot keep trying forever, its failures become contained work items instead of overnight incidents.

FAQ

What is the difference between a call budget and a rate limit for an agent?

A request limit is a cap on attempted external actions, including retries and failed calls. A rate limit controls how quickly actions may occur. You usually need both, because a slow agent can stay under a rate limit and still burn through thousands of calls over several hours.

Should retries count against an autonomous agent's request budget?

Count the retry as a new attempt unless the provider explicitly confirms that it did not receive the request. Retried calls consume provider capacity, time, and often money even when the first attempt failed. A budget that ignores retries gives loops a free path around the limit.

How do I budget an agent that polls for job status?

Use a dedicated budget for poll requests and charge every poll against it. Better still, use webhooks, a callback, or an operation status endpoint with a documented polling interval when the provider offers one. Polling is a frequent source of low-value calls because the agent sees waiting as a problem it can solve by asking again.

How can I cap spend when a vendor reports charges late?

Assign a maximum reservation amount before the workflow starts, then reconcile actual usage after each charge or usage response. Stop when the reservation is exhausted, even if the final invoice arrives later. If a provider cannot expose a credible price signal, treat the action as approval-only or give it a very small call allowance.

Do idempotency keys make external call budgets unnecessary?

No. An idempotency key can prevent duplicate side effects at a cooperative provider, but it does not prevent repeated reads, repeated status checks, token costs, or a fresh request with changed input. Keep idempotency and budgets as separate controls.

How long should an agent be allowed to call external services?

Give the agent a short deadline for the specific operation and a separate deadline for the full run. The operation deadline should reflect normal service latency plus a modest allowance for retries. The run deadline should reflect the human value of the task, not the longest duration an unattended process could tolerate.

What should an AI agent do after it exhausts its budget?

Direct the agent to save the remaining task state, report the budget that stopped it, and return a resumable plan. Do not silently reset the counters or launch a replacement run with a new identity. A stop without a useful record only turns an expensive loop into an investigation.

Do research agents need stricter request limits than deployment agents?

Yes, if the agent can discover new targets, follow links, search broadly, or generate arbitrary request payloads. Static workflows against a small allowlist can use larger limits because their reachable action set is known. Exploratory work needs smaller budgets and frequent human review.

When should a human have to approve an agent's external call?

Human approval should guard a new payee, a destructive action, a large spend reservation, or an expansion of scope. Do not put a person in front of every harmless read request, because people will approve prompts without reading them. Budgets handle volume; approval handles exceptional consequences.

What should an audit log record about an agent budget stop?

Use a signed or hash-chained event record that contains an immutable run identifier, timestamps, attempt sequence, request class, budget decision, and cost estimate. Record a blocked call as carefully as an allowed one. Without denials, you cannot distinguish a safe stop from a broken integration.

Sallyport

Sallyport runs API calls and SSH commands for your AI agent. The keys stay in a local vault on your Mac; you approve each run and every action lands in a sealed journal.

© 2026 Sallyport · Open source under Apache-2.0 · Oleg Sotnikov