budgetkernel
A small, auditable, deterministic budget accounting kernel with zero heap allocation on the hot path.
Declare budgets across fixed dimensions, charge them at runtime boundaries, and get a verdict:
Continue
Warn(dim)
Exhausted(dim)
The crate is intentionally narrow. It does not read clocks, perform I/O, perform heap allocation on the hot path, refill budgets automatically, persist state, or coordinate across machines. The caller owns measurement and policy. budgetkernel owns bounded accounting.
Why this exists
LLM pipelines, task runners, crawlers, quota systems, and agent loops often need to track more than one resource at once:
- tokens
- elapsed milliseconds
- bytes
- calls
- memory
- caller-defined custom units
Most systems do this with ad-hoc counters. budgetkernel provides a small kernel for that accounting with explicit semantics and a well-defined verification story.
It is not a rate limiter. It is not a metrics system. It is not a distributed quota service.
It is the deterministic core those systems can build around.
Example
use ;
For complete examples, see:
Core API
Build a budget:
let mut budget = builder
.limit
.limit_with_warn
.build?;
Charge one dimension:
let verdict = budget.charge?;
Query accounting state:
let spent = budget.spent;
let remaining = budget.remaining;
Manually reset spent counters:
budget.reset;
reset() preserves declared limits and warn thresholds. It does not perform automatic refill. The caller decides when a budget period ends.
Dimensions
The dimension set is fixed:
There are exactly eight dimensions.
The fixed set is deliberate. It avoids dynamic registration, string keys, hashing, allocation, and user-provided discriminants. Internally, dimensions map to dense array indexes.
The three Custom slots are for caller-defined units. For example, an adapter may define Custom0 as "work units" or "retrieval depth" in its own codebase.
Verdicts
Budget::charge returns:
ChargeError reports structural errors, such as charging an undeclared dimension.
Verdict reports the state of an accepted charge:
Continue
The charge was accepted and no configured warn threshold was crossed.
Warn
The charge was accepted, but the running total is now above the configured warn threshold.
Warn is not one-shot. It fires on every charge where the current state is above the warn threshold but not exhausted. If callers want one-shot logging, they should track suppression in their adapter.
Exhausted
The charge pushed the running total past the configured limit.
Limits are inclusive:
spent == limit => Continue or Warn
spent > limit => Exhausted
Exhaustion wins over warning. If one charge crosses both the warn threshold and the limit, the verdict is Exhausted.
Sequential multi-dimension checkpoints
v0.1 intentionally ships single-dimension charging only:
budget.charge?;
Callers who want to check several dimensions at one checkpoint can perform several sequential charges and reduce the verdicts with Verdict::worst:
let mut verdict = Continue;
verdict = verdict.worst;
verdict = verdict.worst;
verdict = verdict.worst;
This is not atomic batch charging. It is a deterministic reduction of sequential results.
Batch charging may be added later if real usage justifies it.
Zero charges and warn = 0
Charging zero is valid:
budget.charge?;
A zero charge reports the verdict for the current state without increasing the reported spent value. This can be used as a state poll.
A warn threshold of zero is also valid. It means the first positive spend enters the warning state.
Design guarantees
The crate is designed around these guarantees:
- no heap allocation on the hot path
- no clocks
- no I/O
- no syscalls
- deterministic behavior
- saturating arithmetic
- bounded work
- no caller-triggerable panics
no_stdcompatibility- one current unsafe boundary, isolated in the internal fixed map implementation
The no-panic guarantee means no caller-triggerable panics from valid API usage. Internal debug_assert! checks may guard kernel invariants during debug builds.
See docs/DESIGN.md for the full design rationale.
Safety model
The default internal map uses MaybeUninit<u64> plus presence bits. This is the only current unsafe boundary.
The safe-map feature replaces that implementation with fully initialized arrays and removes unsafe from the map implementation.
Both variants are tested with the same unit and property tests.
See docs/SECURITY_MODEL.md for invariants, threat model, lint posture, and verification details.
Feature flags
[]
= ["std"]
= []
= []
std
Enabled by default.
Adds std::error::Error implementations for error types.
The core accounting logic does not require std.
safe-map
Uses a fully safe internal fixed map implementation.
This is useful for audits or policy environments that prefer no unsafe code in the internal map. The public behavior is identical.
no_std
Build without default features:
The crate remains usable without std. The caller is still responsible for measurement, clocks, logging, persistence, and any adapter behavior.
Verification
Current verification matrix:
cargo build
cargo build --no-default-features
cargo build --features safe-map
cargo build --no-default-features --features safe-map
cargo test
cargo test --no-default-features
cargo test --features safe-map
cargo test --no-default-features --features safe-map
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --check
cargo doc --no-deps
cargo doc --no-deps --no-default-features
cargo +nightly miri test --lib
The test suite includes:
- deterministic unit tests
- public API property tests
- doctests
- tests under
safe-map - tests under
no_std - MIRI for library tests
Benchmarks
A Criterion benchmark suite is included:
It measures:
- continuing single-dimension charge
- warning single-dimension charge
- already-exhausted single-dimension charge
- three sequential charges as a checkpoint pattern
Benchmark numbers are local measurements and vary by CPU, compiler, target, optimization level, and feature configuration. Treat them as a baseline for your environment, not a universal guarantee.
Non-goals
budgetkernel does not provide:
- automatic time-based refill
- rate limiting
- background tasks
- persistence
- distributed coordination
- async APIs
- dynamic dimension registration
- model pricing tables
- clocks
- I/O
- logging
Adapters can build those behaviors around the kernel.
Status
Released on crates.io.
Current version: 0.1.1.
The core kernel, examples, property tests, benchmarks, design/security docs, README, CHANGELOG, CI, and release packaging are in place.