qubit-budget helps Rust libraries and services put explicit, finite limits on work: bytes read, nodes visited, output produced, open resources, or elapsed time. It keeps the accounting separate from parsing and I/O, so callers can reject oversized work with structured errors and predictable state changes.
Installation
[dependencies]
qubit-budget = "0.5"
The crate has no default features. Enable an integration only when it is needed:
[dependencies]
qubit-budget = { version = "0.5", features = ["json"] }
Available features: json, big-integer, big-decimal (which enables big-integer), and time. The minimum supported Rust version is 1.94.
Quick Start
Suppose one response may contain no more than eight bytes. Charge each accepted chunk to one budget. A failed charge does not change that budget:
use qubit_budget::ResourceBudget;
let mut response = ResourceBudget::new("response-bytes", 8_u64);
response.try_consume(5).expect("the first chunk fits");
let error = response
.try_consume(4)
.expect_err("only three bytes remain");
assert_eq!(error.resource(), &"response-bytes");
assert_eq!(error.limit(), 8);
assert_eq!(error.remaining(), 3);
assert_eq!(error.requested(), 4);
assert_eq!(response.used(), 5);
This is the basic pattern: choose a meaningful resource name, configure a limit at the boundary you own, and let the caller decide how to handle a typed error.
Choose the right primitive
| Need | Type | What happens on success | What happens on failure |
|---|---|---|---|
| Check one value, such as nesting depth | ResourceLimit | No state changes | Reports the observed value and limit |
| Spend an allowance that cannot return | ResourceBudget | Reduces remaining | Budget is unchanged |
| Reuse capacity that callers return | ResourcePool | Acquire or release changes the pool | Pool is unchanged |
| Tie reusable capacity to an owned lifetime | ManagedResourcePool | Returns an RAII permit | Pool is unchanged |
ResourceBudget is not cloneable: copying it would duplicate a finite allowance. ResourcePool is only in-memory accounting; it does not wait, synchronize access, or enforce fairness. ManagedResourcePool is a cloneable, synchronized handle whose permits return capacity on Drop; it does not wait or enforce fairness.
What it provides
- Atomic charges for one budget and all-or-nothing charges for a group.
- Checked conversion of native
usizeandu64measurements. - Structured errors for exceeded limits, insufficient budget, conversion, and invalid pool release.
- Reusable structure limits for depth, nodes, container sizes, and key bytes.
- Transactional string rendering: bytes are charged only after a complete, valid UTF-8 string is produced.
- Optional limits for JSON, strings, big integers, big decimals, durations, and clock-backed deadlines.
With json, an attempt distinguishes immediate I/O accounting from transactional value accounting. Accepted input and writer output remain charged; staged JSON value usage is published only by commit. The user guide explains this boundary with a complete decode scenario.
JSON transaction boundary
Use a transaction to stage measurements for one complete value. It publishes them only after the surrounding operation succeeds:
use qubit_budget::json::JsonMeasurement;
use qubit_budget::json::JsonResource;
use qubit_budget::json::JsonValueLimits;
let mut budget = JsonValueLimits::<JsonResource, usize>::builder()
.max_nodes(8)
.max_string_bytes(16)
.build()
.budget();
let mut transaction = budget.transaction();
transaction.try_admit(JsonMeasurement::String {
depth: 1,
bytes: 5,
})?;
transaction.commit()?;
# Ok::<(), qubit_budget::MeasuredBudgetError<qubit_budget::json::JsonResource, usize>>(())
Raw and normalized input remain immediate charges. A transaction publishes only staged value usage, so callers call commit after the complete value succeeds. A rejected value admission poisons that transaction and prevents publication; already accepted I/O and output remain accounted. The user guide and design document define the complete atomicity matrix and recovery boundary.
What it does not do
This crate does not parse JSON, perform I/O, choose application limit values, wait for pool capacity, or define recovery policy. For JSON parsing and Serde integration, use an adapter such as qubit-json.
Learn More
- User guide: a scenario-led explanation of JSON accounting, errors, transactions, and troubleshooting.
- Design document: invariants, state transitions, and feature boundaries.
- API documentation
Testing
# Run tests with the default feature set
cargo test
# Run tests with all declared features
cargo test --all-features
# Project CI checks
./ci-check.sh
# Check code coverage
./coverage.sh
License
Copyright (c) 2025 - 2026. Haixing Hu. All rights reserved.
Licensed under the Apache License, Version 2.0. See LICENSE for the full license text.
Contributing
Contributions are welcome. Please follow the Rust API guidelines, keep public API documentation and tests current, and run ./align-ci.sh to format code and ./ci-check.sh to satisfy CI requirements before submitting a pull request.
Author
Haixing Hu - Qubit Co. Ltd.
Repository: https://github.com/qubit-ltd/rs-budget