AI coding

Enforcing Budgets Across Child Agents and Sub-Tasks

Antonio Evans8 min read

A parent agent node at the top of a flowchart splits a fixed budget bar into three child bars of different lengths.
Key points

Key points

  1. 1A parent budget must be split before any child agent starts, carving out a parent reserve and giving each child a hard, non-borrowable cap.
  2. 2Debit each child's allocation as a reservation before its first model call, then settle the actual cost and refund the unused portion when it finishes.
  3. 3Provider rate limits and dashboards cannot enforce a per-task budget; you need a session-aware enforcement point between the orchestrator and the model API.
  4. 4At fan-in, reconcile what each branch actually spent and decide whether a child that hit its cap gets reallocated from the reserve or fails the whole branch.

Why fan-out breaks a single-agent budget

A single agent loop has one counter to watch. When a parent fans out, that counter is no longer enough. Each child is a fresh model context that may carry its own system prompt, tool schemas, skill catalogue, and task instructions, and every one of those is billed. An OpenAI Codex issue documents this directly: a workflow split into five subagents can consume more total usage than one serial agent because each child pays the same fixed overhead before doing any unique work (openai/codex issue 39808).

The cost curve compounds inside each child as well. Because many LLM APIs re-bill the whole conversation history on every call, a naive multi-step loop grows input cost quadratically: step N re-sends steps 1 through N-1 (token cost of agent loops). If you fan out ten children, you are multiplying that curve ten times over.

Worse, agents do not fail loudly when they overspend. They keep calling tools, retrying, and spawning more work until something external stops them. A catalog of 63 confirmed production budget-overrun incidents across 21 orchestration frameworks describes this as a documented failure class, where a single retry loop can accumulate thousands of dollars before an operator notices (arXiv token budgets study). The only reliable fix is to bound spend before the call, not after.

The one decision that matters: reserve before you spend

The core pattern is admission control. Before each child makes a model call, the controller prices the worst-case cost of that call, holds it against the child's allocation, issues the call, then settles the actual charge and refunds the difference. The check is prospective, so a request whose worst case exceeds the remaining balance is refused before any money leaves (per-run budget reservation pattern).

Three properties make this work:

  • Pre-execution: the decision happens before the model call, tool invocation, or side effect.
  • Atomic: the check and the deduction are one operation, so two concurrent children cannot both read the same balance and both proceed.
  • Cumulative: reservations and settlements track across retries and concurrent workers, not just request velocity.

The last property is where naive in-app counters fail. A counter in a variable or a database row is not atomic; two concurrent requests can both see "budget remaining: $10," both proceed, and spend $20. It is also lost on crash and does not survive retries or process restarts (hard spend limits).

So the first decision you make is architectural, not arithmetic: your budget must live in a store that supports atomic compare-and-set against a session identifier, whether that is Redis, Postgres with row locks, or a dedicated control-plane service.

Splitting the parent budget: equal, weighted, or on-demand

Once you have an enforcement point, you have to decide how the parent budget divides. Three strategies cover most cases, and the right choice depends on what you know before fan-out.

StrategyHow it dividesBest forMain risk
Equal split(parent - reserve) / N per childN similar, independent tasks (e.g. summarize 8 documents)One hard child starves while easy children leave budget unused
Weighted splitProportional to an upfront estimate per childTasks with known differing difficulty (big repo vs. one file)Estimates are wrong; strong children hit caps early
On-demandEach child draws from a shared float as it startsUnknown number of children, sequential or streaming fan-outNo per-child ceiling unless you also impose one

The short answer applies a hybrid: a parent reserve of roughly 20 percent, the remainder partitioned by whichever strategy fits, and every child given its share as a hard ceiling regardless of strategy.

Here is the arithmetic for a worked example. A parent ticket has a budget of $2.00. You hold back 20 percent, so $0.40 sits in the parent reserve and $1.60 is allocatable.

  • Equal split across 4 children: $0.40 each.
  • Weighted split with estimate weights 4, 2, 1, 1: total weight 8, so the large child gets $0.80, the next $0.40, and the two small ones $0.20 each.
  • On-demand: each child requests a denomination from the $1.60 float, and the controller grants it only if the float still covers the request.

The reserve is not wasted. It is the only budget the parent has left to do its own synthesis after fan-in, and it is the pool you reallocate from when a child legitimately needs more.

Inheriting hard caps so no child can exceed its share

The inheritance rule is the part teams get wrong. A child must receive its allocation as a hard ceiling it cannot raise on its own, and it must be unable to borrow from siblings. Two failure modes appear when this rule is soft:

  • Cross-subsidy: a runaway child quietly drains the pool it shares with its siblings, and the well-behaved children fail for lack of budget.
  • Ambient capability: a child spawned by another child inherits no scoped limit and spends against whatever global account it can reach.

The defense is to make the cap a property of the child's context, not a hope about its behavior. When the orchestrator spawns a child, it passes the child a budget handle that is bound to a specific allocation and a specific session identifier. The child's model calls all route through the same controller, which reads the handle and enforces the cap.

The minting model is worth copying: raw budget is created only by minting from a parent allocation, and budget creation is what draws down the parent's remaining balance. Children receive fragments, never the ability to mint (budget primitives). That makes a child that tries to exceed its cap fail at the controller, not at the model.

To make the child's own prompt cooperate, state the cap explicitly in the delegation message. A subagent starts with a fresh, isolated context and sees only the prompt string the parent writes, so if the child needs to know its budget, that number must be in the briefing (subagent orchestration guide).

Reserving and refunding: the accounting loop

The loop for each child is short, but every step has a failure mode. The controller holds spent, pending, and maximum for the child, and the invariant is that maximum - spent - pending must cover the next request's worst case (per-run budget reservation).

# Pseudocode: controller-side settlement for one child call
def run_child_call(child_id, worst_case, do_call):
    with atomic_lock(child_id):                      # check + reserve in one op
        child = store.get(child_id)
        if child.maximum - child.spent - child.pending < worst_case:
            raise BudgetExceeded(child_id)           # fail closed, before the call
        child.pending += worst_case
        store.put(child)                             # persisted before dispatch

    try:
        result, usage = do_call()                    # the actual model call
    except Exception:
        with atomic_lock(child_id):
            store.refund(child_id, worst_case)       # release the whole reservation
        raise

    actual = price(usage)
    with atomic_lock(child_id):
        store.settle(child_id, reserved=worst_case, actual=actual)

def settle(child_id, reserved, actual):
    if actual > reserved:
        raise UncertainCharge(child_id)              # unverified = overspend
    store.apply(child_id, spent_delta=actual, pending_delta=-reserved)
    # the delta (reserved - actual) returns to the child's allocation automatically

Two details matter. First, price every input token at the highest applicable rate during the reservation so the worst case is truly worst case, then refund down to the actual mix at settlement; the reference example reserves the cache-write ceiling and settles lower once real usage is known (per-run budget reservation). Second, treat a settlement where actual exceeds reserved, or where usage is malformed, as a permanently blocked state rather than a warning. An unverifiable charge is an overspend you cannot account for.

Refunds are not an optional optimization. Without them, worst-case reservations compound turn over turn and the child's effective working budget collapses to the maximum divided by the worst-case ratio after a handful of calls.

Handling overruns when a child hits its cap

Sooner or later a child reaches its ceiling. What happens next should be a policy decision made in advance, not an improvised one at runtime. Four responses are defensible:

  1. Stop and fail the branch. The child halts, its partial output is checkpointed, and the parent records the branch as incomplete.
  2. Reallocate from the parent reserve. The parent lends the child additional budget drawn from the 20 percent reserve, subject to the reserve still having headroom.
  3. Degrade gracefully. Throttle the child, reduce output verbosity, or switch to a cheaper tier for its remaining work.
  4. Reject the next call only. The child finishes its current step and reports a partial result rather than aborting mid-step.

Production practice favors graceful degradation over a single hard cutoff, because throttling or checkpointing preserves partial task progress that termination would discard (token budget controls). Whatever you choose, pair it with loop detection. Track call signatures (tool name plus arguments) so repeated or near-identical calls surface as loops rather than only as rising volume.

Distinguish a legitimate overrun (a branch that is genuinely larger than estimated) from a runaway (a loop, a retry storm, or a self-invoked subagent). Recursive tool calls, retry storms, self-invoked subagents, and provider rate limits applied uniformly are the four patterns that account for most uncontrolled spend (token budget controls).

Reconcile at fan-in and instrument what you enforce

Fan-in is where the accounting closes. Sum what each child actually settled, compare it to what each was reserved, and add the parent's own spend plus whatever it drew from the reserve. Two rules keep this honest:

  • A child's unused refund flows back to the parent's remaining balance, not into a sibling's allocation mid-run.
  • Any difference between reserved and settled above a tolerance should be logged with the child identifier and the call signature that produced it.

Attribute every model and tool call with a session or task identifier, because provider dashboards report aggregate usage and do not break it down per agent session by default (token budget controls). OpenTelemetry's GenAI semantic conventions give you standard attribute names for token usage and model identity, so per-child cost shows up in a trace you can inspect rather than a spreadsheet you reconstruct (OpenTelemetry for Generative AI). Mapping each child to its own span with input and output token counts makes the fan-out tree legible at a glance.

The telemetry itself has a privacy constraint: store prompt and completion content in span events, not span attributes, because attributes are indexed and can expose PII in your backend (OpenTelemetry for AI systems).

If you are standardizing this across many agents, a single inference layer that returns usage on every call and prices everything in one currency removes a lot of the reconciliation work; Quanta Inference exposes text, image, video, audio and embedding models behind one API key, so per-call cost attribution is uniform across providers (Quanta Inference).

A short operator checklist

Run through this before you ship fan-out:

CheckWhy it matters
Budget lives in an atomic store keyed by session or child idIn-app counters race and lose state on restart
Parent reserve carved out before any child startsParent still has budget for synthesis and reallocation
Every child has a hard ceiling it cannot raiseStops cross-subsidy and runaway children
Reservation happens before dispatch, settlement refunds the deltaBounds worst case without starving utilization
Overrun policy chosen in advance (fail, reallocate, degrade)Prevents improvised decisions under pressure
Loop detection keyed on call signaturesSeparates legitimate long work from stuck repetition
Per-child span with token countsMakes fan-in reconciliation possible
FAQ

FAQ

Can a child agent just ask for more budget?

Only if your policy allows it. Design the controller so a child can request a top-up from the parent reserve, but the grant is made by the orchestrator, not by the child itself. A child that mints its own budget can bypass every cap you set, which is exactly the race the runaway-spend incidents describe.

What happens to a child's unspent allocation?

It stays with that child's allocation and flows back to the parent balance at fan-in. If you instead push it into a shared float mid-run, a sibling can consume it before you have reconciled, and your per-child ceilings stop meaning anything. Keep refunds to the child, then reconcile upward.

Should I use equal, weighted, or on-demand splitting?

Use equal splits for N similar independent tasks, weighted splits when you have defensible upfront estimates, and on-demand when the number of children is unknown. Most production systems combine a weighted split for the large branches with on-demand draws for small, unpredictable ones.

How do I tell a legitimately expensive branch from a runaway?

Track call signatures, not raw call volume. A long but productive research branch issues many distinct calls, while a broken one issues a few identical ones. A loop detector keyed on tool name plus arguments flags the second without penalizing the first.

What if actual cost exceeds the reserved amount?

Fail closed. Treat the mismatch as an unverified charge, block that child, and log the call signature. Reservations should already price at the worst-case rate, so an overshoot means either your pricing table is stale or the provider reported usage you did not expect, and both cases need a human to look.

For teams that would rather not rebuild the control plane from scratch, Quanta Skills lets you install a reusable budget and orchestration skill into a coding agent, and experts can run the reconciliation for a line of business (Quanta Skills).

Sources

Sources

  1. Subagent fan-out can increase usage because each agent ...
  2. AI Agent Loop Token Costs: How to Constrain Context
  3. Token Budgets: An Empirical Catalog of 63 LLM-Agent ...
  4. Per-Run Budget Reservation for Coding Agent Model Calls
  5. AI Agent Budget Control: Enforce Hard Spend Limits - Cycles
  6. Claude Code Subagents and Multi-Agent Orchestration ...
  7. Token Budget Controls for AI Agents: Implementation Guide
  8. OpenTelemetry for Generative AI
  9. OpenTelemetry for AI Systems: LLM and Agent ...
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles