Atomic, hierarchical resource budgets for concurrent Rust task trees.
Bound tokens, tool calls, API requests, bytes, retries, cost estimates, deadlines, and any other integer resource.
budget-context is a dependency-light Rust crate for enforcing cumulative,
process-local resource limits across concurrent and asynchronous work. A child
task may impose a tighter budget, but it cannot escape its parent's limits.
Accounting and reservations remain atomic across the complete task hierarchy.
It is designed for AI agents, autonomous workflows, web crawlers, batch jobs, request-scoped quotas, and any Rust system where the total amount of work is unknown before execution begins.
Project status: early-stage
0.1.x. The API may evolve before1.0.
- Why budget-context?
- Core capabilities
- Installation
- Quick start
- Multi-resource reservations
- Tokio cancellation and deadlines
- Where it fits
- How it compares
- Safety and non-goals
- Feature flags
- Performance
- Development
Long-running and autonomous programs rarely follow a predictable path:
root workflow: 100k tokens, 50 tool calls, 5 minute deadline
│
├── research agent: 25k tokens, 15 searches
│ └── web worker
│
├── coding agent: 60k tokens, 20 test runs
│ └── test worker
│
└── reviewer: 20k tokens
Checking remaining() before doing work is racy: another task may consume the
same capacity between the check and the update. Independent counters also fail
when sibling tasks collectively exceed a shared parent limit.
budget-context solves those two problems with one primitive:
Every successful resource operation is validated and recorded atomically at the current node and every ancestor.
For each limited resource, the crate preserves:
consumed + reserved <= limit
- Arbitrary resources — applications define names such as
llm.tokens,agent.tool_calls,http.requests, orcost.usd_micros. - Hierarchical limits — children inherit ancestor constraints and may add tighter local ceilings.
- Atomic accounting — concurrent consume and reserve operations cannot oversubscribe a resource.
- RAII reservations — unused capacity is automatically released when a reservation is dropped.
- Multi-resource transactions — reserve tokens, requests, and estimated cost together, or reserve none of them.
- Fail-closed reconciliation — reported usage above a reservation retains the full reservation as consumed and reports the unaccounted overage.
- Consistent snapshots — inspect consumed, reserved, locally limited, and effectively remaining capacity at one instant.
- Deadlines — child deadlines may shorten, but never extend, an ancestor's deadline.
- Optional Tokio integration — directional cancellation propagation and deadline-aware future execution.
- Optional Serde and tracing — serialize observational data and emit structured accounting events without coupling to a backend.
Add the core crate from crates.io:
cargo add budget-contextEnable integrations only when needed:
cargo add budget-context --features tokio,serde,tracingFor a local checkout or workspace under development:
[dependencies]
budget-context = { path = "../budget-context" }The accounting core does not require an async runtime.
use budget_context::{Budget, Resource};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let tokens = Resource::new("llm.tokens")?;
let tool_calls = Resource::new("agent.tool_calls")?;
let task = Budget::builder()
.name("coding-task")
.limit(tokens.clone(), 100_000)
.limit(tool_calls.clone(), 50)
.build()?;
let researcher = task
.child()
.name("researcher")
.limit(tokens.clone(), 25_000)
.build()?;
researcher.consume(&tool_calls, 1)?;
let reservation = researcher.reserve(&tokens, 8_000)?;
// Constrain the external operation to the reserved maximum.
let actual_tokens = 3_421;
reservation.commit(actual_tokens)?;
println!("{:#?}", researcher.snapshot());
Ok(())
}remaining() and snapshots are observational. Only consume(), reserve(),
and reserve_many() authorize work atomically.
Operations often consume several resources together. An LLM request may need one request slot, a token allowance, and an estimated-cost allowance:
# use budget_context::{Budget, Resource};
# fn example() -> Result<(), Box<dyn std::error::Error>> {
# let tokens = Resource::new("llm.tokens")?;
# let requests = Resource::new("llm.requests")?;
# let cost = Resource::new("cost.usd_micros")?;
# let budget = Budget::builder()
# .limit(tokens.clone(), 100_000)
# .limit(requests.clone(), 10)
# .limit(cost.clone(), 2_000_000)
# .build()?;
let permit = budget.reserve_many([
(&requests, 1),
(&tokens, 8_000),
(&cost, 100_000),
])?;
permit.commit([
(&requests, 1),
(&tokens, 3_421),
(&cost, 42_000),
])?;
# Ok(())
# }Acquisition and reconciliation are atomic across every requested resource and
every node in the lineage. Duplicate entries are combined using checked
arithmetic. Dropping permit without committing releases all reserved capacity.
If an external operation reports 9,000 tokens after reserving 8,000, the crate
cannot admit the extra 1,000 retroactively without breaking an ancestor limit.
It therefore converts the full 8,000 reservation to consumed capacity and
returns ReservationExceeded with the 1,000-token unaccounted overage.
Applications requiring a hard bound must also configure the underlying operation—such as a model's maximum output tokens—to stay within the reserved amount.
Enable the tokio feature:
[dependencies]
budget-context = { version = "0.1", features = ["tokio"] }Then run a future until it completes, the budget is cancelled, or its effective deadline expires:
# use std::time::Duration;
# use budget_context::Budget;
# async fn operation() -> Result<u64, std::io::Error> { Ok(42) }
# async fn example() -> Result<(), Box<dyn std::error::Error>> {
let budget = Budget::builder()
.deadline_after(Duration::from_secs(30))
.build()?;
let value = budget.run(operation()).await??;
# let _ = value;
# Ok(())
# }run() preserves the future's output, so a fallible future produces a nested
Result. Cancellation wins over deadline expiration, which wins over future
completion when multiple branches are ready together.
Cancelling a parent cancels its descendants. Cancelling a child does not cancel its parent or siblings. Existing reservations may still reconcile or release after cancellation so accounting is never abandoned halfway through.
Good uses include:
- AI agents and autonomous workflows — tokens, model requests, tool calls, searches, shell commands, test runs, and estimated cost.
- MCP and plugin hosts — bound operations at host-controlled invocation boundaries.
- Web crawlers — HTTP requests, downloaded bytes, pages, and recursion.
- Server requests — database queries, downstream API calls, response bytes, and request deadlines.
- Batch processing — records, retries, errors, external calls, and speculative work.
- Recursive algorithms — depth, nodes, expansions, or generated items.
| Tool | Best for | Hierarchy | RAII reservations | Replenishes over time | Waits fairly |
|---|---|---|---|---|---|
| budget-context | Cumulative task-tree budgets | yes | yes | no | no |
qubit-budget |
Lightweight single-dimension accounting | no | no | no | no |
governor |
Rate limiting and replenishing quotas | no | no | yes | optional async wait |
tokio::sync::Semaphore |
Concurrent in-flight work | no | permit on drop | reusable permits | yes |
CancellationToken |
Hierarchical cancellation | cancellation only | no | n/a | n/a |
These tools are complementary. Use a rate limiter for requests per second, a
semaphore for maximum in-flight operations, and budget-context for the total
amount of work allowed in one execution tree.
budget-context is cooperative. Code must receive and consult a Budget to be
constrained. It does not sandbox untrusted code or replace authorization.
The crate is intentionally not:
- a distributed or persistent quota service
- a monthly user or organization quota system
- a billing or audit ledger
- an API rate limiter
- a fair concurrency limiter or waiting queue
- a permission system or security boundary
- a mechanism for measuring CPU time or memory exactly
- a way to retroactively prevent external usage beyond an unenforced estimate
Live Budget values are not serializable. They contain process-local clocks,
locks, hierarchy state, and optional cancellation tokens. Read-only snapshots
can be serialized with the serde feature.
| Feature | Default | Adds |
|---|---|---|
serde |
no | Serialization for resources, snapshots, and errors |
tokio |
no | Hierarchical cancellation and Budget::run() |
tracing |
no | Structured consume, reserve, commit, release, and cancel events |
The exact hierarchy, error ordering, reconciliation, snapshot, and runtime semantics are recorded in DESIGN.md.
Accounting is O(hierarchy depth × resource count) and locks each lineage in a
stable root-to-leaf order. The initial implementation prioritizes correctness,
deadlock prevention, and deterministic errors over lock-free complexity.
The included Criterion benchmarks cover:
- hierarchy depths 1, 2, 5, and 10
- single and multi-resource reservations
- contention with 1, 4, 16, and 64 workers
Use operation-level accounting rather than calling the crate for every byte or item in a very hot loop. No fairness guarantee is made between sibling tasks.
The repository forbids unsafe Rust and checks all public documentation.
cargo fmt --all --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features
cargo test --no-default-features
cargo hack check --feature-powerset --no-dev-deps
cargo bench --all-features
cargo doc --all-features --no-deps
cargo package
cargo llvm-cov --all-features --workspace --fail-under-lines 95The test suite includes accounting, hierarchy, reservation, concurrency, Tokio, Serde, tracing, public API, property, and reduced Loom-model coverage. CI tests stable Rust on Linux, macOS, and Windows, checks the Rust 1.85 MSRV, runs both feature matrices and every feature combination, guards the public API against SemVer regressions, compiles benchmarks and README examples, and enforces a 95% line-coverage floor.
Contributions are welcome while the API is being shaped. Please include tests for semantic changes and preserve the invariants in DESIGN.md.
Licensed under either of:
at your option.
