Small, zero-dependency Rust reliability crates: explicit invariants, redacted
secrets, bounded inputs, deterministic data, and runtime-agnostic resilience.
no_std-friendly, no unsafe, adopt one crate at a time.
Reliakit is a workspace of small, focused crates for building reliable Rust software: CLIs, services, bots, libraries, and infrastructure tools. The core idea is simple: validate and constrain data at the boundary, then carry the trusted invariant deeper into your program so the rest of the code cannot hold an invalid state.
It is a general-purpose reliability toolkit. Validated primitives, secret redaction, bounded collections, deterministic encoding, and runtime-agnostic resilience utilities (retry backoff, circuit breaker, rate limiter, timeouts) are useful in web backends, command-line tools, embedded code, data pipelines, and protocol or blockchain work alike. None of those is the primary target.
Every crate is small, dependency-free at runtime (only the standard library and
other reliakit-* crates; a CI check fails the build if any third-party
dependency appears), #![forbid(unsafe_code)], and usable on its own. You adopt
one crate at a time, not a framework.
Have a problem in mind? The cookbook maps it to a crate and the smallest correct example.
| Problem | Use |
|---|---|
| Validate external input | reliakit-primitives, reliakit-validate |
| Keep secrets out of logs | reliakit-secret |
| Retry fallible operations | reliakit-retry, reliakit-backoff |
| Limit how fast a worker runs | reliakit-ratelimit |
| Encode data deterministically | reliakit-codec |
| Stop calling a failing dependency | reliakit-circuit |
| Give an operation a time budget | reliakit-timeout |
| Parse strict, bounded JSON or CSV | reliakit-json, reliakit-csv |
| Cap concurrent in-flight work | reliakit-bulkhead |
| Aggregate checks for a health endpoint | reliakit-health |
| Weigh signals into an explainable decision | reliakit-decide |
| Bound a queue, buffer, or cache | reliakit-collections |
For choosing between the resilience blocks, see Which resilience block do I use? below.
Retry a flaky operation with backoff, with no runtime, no sleeping, no third-party dependencies:
use core::time::Duration;
use reliakit_retry::{retry, Backoff, RetryError, RetryPolicy};
let policy = RetryPolicy::new(3, Backoff::constant(Duration::from_millis(10))).unwrap();
let mut calls = 0;
let result: Result<u32, RetryError<&str>> = retry(
&policy,
|| {
calls += 1;
if calls < 2 { Err("temporary") } else { Ok(42) }
},
|_error| true, // retry every error
);
assert_eq!(result.unwrap(), 42);[dependencies]
reliakit-retry = "1"Reliakit is zero third-party dependencies, no_std, and runtime-agnostic. No
async runtime is baked in and nothing sleeps on its own. You inject the clock or
sleeper, so the same code runs synchronously, under any async runtime, or in a
test with no real time. You provide the time source; in return the code stays
dependency-free, portable to embedded targets, and deterministic to test.
- Validate once, at the boundary. Construct a typed value where data enters your program (config, request, CLI, environment) and never re-check it again.
- Make invalid states hard to represent. A
Portis always1..=65535; aBoundedStr<3, 32>always has 3–32 characters. The type signature documents and enforces the rule for you. - Stop leaking secrets. Wrap sensitive values in
Secret<T>/SecretStringso they render as[REDACTED]inDebug,Display, logs, and error reports. - Bound your inputs and collections.
BoundedVec<T, MIN, MAX>cannot be built outside its size limits. - Encode data deterministically.
reliakit-codec(binary) andreliakit-json(text) produce the same bytes for the same value, handy for cache keys, fixtures, hashing, and signing. - Handle resilience explicitly. Backoff, circuit breaking, rate limiting, and timeouts are plain values you pass the current time into, with no runtime, no hidden threads, no global state.
- Keep adoption cost low. Small independent crates compile fast and pull in nothing extra.
- One cohesive family: take one or all. Use a single crate for one job, or
the
reliakitumbrella for several; every block follows the same conventions and the same zero-dependency,no_std, no-unsaferules. Reliability patterns usually mean stitching together unrelated crates with different designs and dependency trees; here they are built to fit.
Adding Reliakit is close to free; the costs you usually weigh before taking on a dependency mostly aren't here:
- Zero third-party dependencies. With every feature enabled, the entire
dependency tree is
reliakit-*crates and the standard library, with nothing else to vet, audit, or track for security advisories. A CI check fails the build if a third-party crate ever appears, andcargo tree -p reliakit --all-featuresproves it. - No
unsafe. Every crate declares#![forbid(unsafe_code)]. no_std-friendly. The core crates build for bare metal (for examplethumbv7em-none-eabi);allocandstdare opt-in features.- Fast cold builds. There is no third-party graph to compile, so you build Reliakit and nothing else.
- Small, readable surface. Each crate does one thing and is small enough to read end to end before you depend on it.
- Pay only for what you use. Take a single crate, or pull several through the
reliakitumbrella behind per-crate feature flags.
| Area | Crate(s) | What you get |
|---|---|---|
| Validated primitives | reliakit-primitives |
Port, Email, HttpUrl, Hostname, BoundedStr, Percent, SemVer, Uuid, HumanDuration, … |
| Secret redaction | reliakit-secret |
Secret<T>, SecretString, opt-in expose_secret |
| Validation traits | reliakit-validate |
Validate trait, ValidationError that collects every field violation |
| Bounded collections | reliakit-collections |
BoundedVec<T, MIN, MAX> with enforced size invariants |
| Canonical binary codec | reliakit-codec |
CanonicalEncode / CanonicalDecode, strict decoding |
| Strict JSON | reliakit-json |
Strict parser + limits, deterministic output, typed JsonEncode / JsonDecode |
| Strict CSV | reliakit-csv |
Strict, bounded reader + deterministic writer, typed CsvEncode / CsvDecode |
| Resilience | reliakit-backoff, reliakit-bulkhead, reliakit-circuit, reliakit-ratelimit, reliakit-timeout |
Retry backoff, concurrency limiter, circuit breaker, token-bucket rate limiter, deadlines, all clock-agnostic |
| Retry helper | reliakit-retry |
RetryPolicy + retry / retry_with_sleep / retry_async; runtime-agnostic, never sleeps internally |
| Health reporting | reliakit-health |
Health status + criticality-aware aggregator for /health, probes, and status pages |
| Shared clock | reliakit-core |
Clock trait + ManualClock / MonotonicClock |
| Derive helpers | reliakit-derive |
#[derive(CanonicalEncode, CanonicalDecode, JsonEncode, JsonDecode)] |
| Decision logic | reliakit-decide |
Deterministic utility-based decisions (Reasoner with decide/explain/gate/Policy) |
The resilience crates each solve one problem, and each is a plain value you drive with the current time, with no runtime, no hidden threads, no global state. Pick by the question you are asking:
| Question | Block | Crate |
|---|---|---|
| How long should I wait between retries? | backoff delays + jitter | reliakit-backoff |
| Retry a fallible call with an attempt limit? | retry driver (sync + async) | reliakit-retry |
| Stop calling a dependency that keeps failing? | circuit breaker | reliakit-circuit |
| Cap how often something may happen? | token-bucket rate limiter | reliakit-ratelimit |
| Cap how many run at once, and shed the rest? | concurrency limiter (bulkhead) | reliakit-bulkhead |
| Has the time budget for this operation run out? | deadline / timeout | reliakit-timeout |
They compose rather than overlap: retry drives backoff between attempts;
circuit stops calling a dependency once it has failed enough; ratelimit and
bulkhead shed load before you start (too often / too many at once); and timeout
bounds the whole operation. None of them sleep or spawn for you; you pass the
clock (or a sleeper) in, so they stay runtime-agnostic and trivial to test.
The resilient_client example shows
a timeout, a rate limiter, a circuit breaker, and retry-with-backoff cooperating in
a single call.
Validate request fields into typed values once, near the edge:
use reliakit_primitives::{Email, Port};
let contact = Email::new("ops@example.com")?;
let port = Port::new(8080)?;
assert_eq!(contact.domain(), "example.com");
assert_eq!(port.get(), 8080);Turn loosely-typed config into trusted types, and keep credentials out of logs:
use reliakit_primitives::{BoundedStr, Percent, Port};
use reliakit_secret::{ExposeSecret, SecretString};
type ServiceName = BoundedStr<3, 32>;
let name = ServiceName::new("api-service")?;
let success_rate = Percent::new(99)?;
let port = Port::new(8080)?;
let api_key = SecretString::from_string("rk_live_example");
assert_eq!(api_key.to_string(), "[REDACTED]"); // never leaks in Display/Debug/logs
assert_eq!(api_key.expose_secret(), "rk_live_example"); // explicit opt-in to read itClock-agnostic resilience values you drive with your own time source:
use reliakit_ratelimit::RateLimiter;
use reliakit_circuit::{CircuitBreaker, State};
// Allow bursts of up to 10, refilling 1 token every 100 ms (~10/sec).
let mut limiter = RateLimiter::new(10, 1, 100);
assert!(limiter.try_acquire_one(0));
// Trip after 3 consecutive failures; stay open for 30_000 ms.
let mut breaker = CircuitBreaker::new(3, 30_000);
for _ in 0..3 {
let _ = breaker.allow(0);
breaker.on_failure(0);
}
assert_eq!(breaker.state(), State::Open); // fail fast instead of hammering a down serviceuse reliakit_codec::{decode_from_slice_exact, encode_to_vec};
use reliakit_derive::{CanonicalDecode, CanonicalEncode};
#[derive(Debug, PartialEq, CanonicalEncode, CanonicalDecode)]
struct Record { id: u64, ok: bool }
let bytes = encode_to_vec(&Record { id: 7, ok: true })?;
assert_eq!(decode_from_slice_exact::<Record>(&bytes)?, Record { id: 7, ok: true });use reliakit_derive::{JsonDecode, JsonEncode};
use reliakit_json::{from_json_str, to_json_string};
#[derive(Debug, PartialEq, JsonEncode, JsonDecode)]
struct Event { id: u64, name: String }
let json = to_json_string(&Event { id: 1, name: "deploy".into() });
assert_eq!(json, r#"{"id":1,"name":"deploy"}"#);
assert_eq!(from_json_str::<Event>(&json).unwrap(), Event { id: 1, name: "deploy".into() });The resilience crates and the allocation-free primitives work without std or
even alloc. A CircuitBreaker or RateLimiter is a small Copy value with
saturating, panic-free integer math; you pass a u64 tick in, so it runs on
embedded targets just as well as on a server.
Because reliakit-codec defines one canonical byte representation per type and
reliakit-json can emit RFC 8785 (JCS) canonical JSON (opt-in canonical
feature), the same value always produces the same bytes, useful for cache keys,
content addressing, and hashing or signing in protocol and blockchain work. This
is one use case among many, not the focus.
reliakit-health turns per-component status into one answer for a /health or
/readyz endpoint or a status page. You build a HealthReport from critical
and optional checks, and the aggregate is criticality-aware: an optional
dependency (say a cache) being Unhealthy degrades the service rather than
failing it, while a critical one (the database) fails it. It only reports; it
never retries, sleeps, or acts.
reliakit-decide is a small deterministic decision engine for when an if/else
is too blunt. A Reasoner scores candidate Actions from weighted
Considerations shaped by a Curve, with gate(...) for hard constraints (an
option that is down or rate-limited is skipped entirely) and explain() for why
a choice won, useful for request routing, picking a backend, or deciding when an
agent should call an LLM. Same inputs, same decision, every time.
The quickest way in is the umbrella crate reliakit, which re-exports every
building block behind a feature flag. Add one dependency and enable only the
pieces you want:
[dependencies]
reliakit = { version = "1.1", features = ["ratelimit", "secret"] }use reliakit::ratelimit::RateLimiter;
use reliakit::secret::Secret;Nothing is pulled in beyond the features you enable, so the zero-dependency,
no_std-friendly nature of each block is preserved. Use features = ["full"]
for everything.
Prefer the tightest possible dependency graph? The crates are fully independent; depend on just the ones you need:
[dependencies]
reliakit-primitives = "1.1"
reliakit-secret = "1.0"
reliakit-validate = "1.0"
reliakit-collections = "1.0"
reliakit-codec = "1.0"
reliakit-json = "1.0"
reliakit-csv = "1.0"
reliakit-backoff = "1.1"
reliakit-retry = "1.1"
reliakit-bulkhead = "1.1"
reliakit-health = "1.0"
reliakit-circuit = "1.1"
reliakit-ratelimit = "1.0"
reliakit-timeout = "1.0"
reliakit-core = "1.0"
reliakit-derive = "1.1"
reliakit-decide = "1.0"Each crate is independent; most projects use two or three. The minimum supported Rust version is 1.85.
| Crate | Purpose | Use when | Status |
|---|---|---|---|
reliakit-primitives |
Validated primitive types | You want Email, Port, Percent, BoundedStr, … instead of unchecked strings/numbers. |
Published (1.1) |
reliakit-secret |
Secret redaction wrappers | A value must not leak through Debug/Display/logs. |
Published (1.0) |
reliakit-validate |
Validation trait + error aggregation | You want to collect every field error at once. | Published (1.0) |
reliakit-collections |
Bounded collection types | A collection must stay within a fixed size range. | Published (1.0) |
reliakit-codec |
Canonical binary encoding/decoding | You need deterministic bytes (cache keys, fixtures, framing). | Published (1.0) |
reliakit-json |
Strict, deterministic JSON + typed encode/decode | You parse untrusted JSON or need predictable output. | Published (1.0) |
reliakit-csv |
Strict, deterministic CSV + typed encode/decode | You parse untrusted CSV or need reproducible output. | Published (1.0) |
reliakit-backoff |
Retry backoff delays + jitter | You retry an operation and want explicit spacing. | Published (1.1) |
reliakit-retry |
Runtime-agnostic retry helper (sync + async) | You retry fallible operations and want attempt limits, backoff, and an error classifier without forcing a runtime. | Published (1.1) |
reliakit-bulkhead |
Concurrency limiter (counting semaphore) | You cap how many operations run at once and shed the rest. | Published (1.1) |
reliakit-health |
Health status + criticality-aware aggregator | You expose a /health/readyz endpoint or status page. |
Published (1.0) |
reliakit-circuit |
Circuit breaker state machine | You want to stop calling a failing dependency. | Published (1.1) |
reliakit-ratelimit |
Token-bucket rate limiter | You cap how often something may happen. | Published (1.0) |
reliakit-timeout |
Deadlines / time budgets | You track whether a budget has run out. | Published (1.0) |
reliakit-core |
Shared Clock trait + clocks |
You want a ready-made u64 time source for the resilience crates. |
Published (1.0) |
reliakit-derive |
Derive macros for codec + JSON traits | You want #[derive(...)] instead of hand-writing encode/decode. |
Published (1.1) |
reliakit-decide |
Deterministic utility decision engine | You want graded, explainable, testable decisions (routing, selection, when to call an LLM). | Published (1.0) |
The resilience crates (backoff, bulkhead, circuit, ratelimit, timeout)
are clock-agnostic; you pass the time in (where they need it), so they
compose and work in sync, async, and embedded code: a rate limiter decides
whether to call, a bulkhead bounds how many calls run at once, a circuit breaker
stops calling a failing dependency, backoff spaces out retries, and a timeout
bounds how long you wait.
- Small, independent crates you adopt one at a time, with no framework lock-in.
- Explicit invariants validated at construction; invalid states are hard to represent.
- Boring, predictable APIs: plain types and traits, no hidden runtime, threads, or global state.
- Zero runtime dependencies (standard library + other
reliakit-*crates only) and#![forbid(unsafe_code)]throughout. - Deterministic behavior: same input, same output; saturating arithmetic in the resilience crates.
- Feature-gated integrations: cross-crate links (e.g. codec ↔ primitives, JSON ↔ validate) are opt-in features, never default.
- Validating config, CLI flags, environment, or request payloads at the boundary.
- Backend services, bots, and libraries that need small typed constraints.
- Keeping secrets out of logs and diagnostics.
- Deterministic encoding for cache keys, fixtures, protocols, or signing.
- Adding explicit retry/backoff/rate-limit/circuit-breaker/timeout logic without pulling in an async runtime.
- Embedded or
no_stdcode that needs constrained values or resilience math.
Reliakit is a set of small building blocks, not a platform. Reach for something else when you need:
- a full web framework, HTTP stack, or async runtime integration;
- a complete serialization ecosystem with format plugins and zero-copy deserialization;
- schema validation, query/database tooling, or an ORM;
- domain-specific validators beyond Reliakit's intentionally narrow checks
(its
Email/HttpUrlvalidation is pragmatic, not a full RFC implementation).
Reliakit is no_std-friendly where it makes sense, but the details differ per
crate; check each crate's README for the exact flags.
- Default features enable
std, which impliesalloc. Building with--no-default-featuresgives theno_stdsubset. - Allocation-backed APIs need
alloc. Owned types (String/Vec-backed, e.g.Email,BoundedStr,SecretString,BoundedVec, all ofreliakit-jsonandreliakit-csv) require theallocfeature; the allocation-free primitives (Port,Percent,Uuid,MacAddress,HumanDuration, numeric types) work with neither. - The resilience crates are pure
core.reliakit-backoff,reliakit-retry,reliakit-circuit,reliakit-ratelimit,reliakit-timeout, andreliakit-coreneed no allocation at all.circuit,ratelimit, andtimeoutoffer an optionalcorefeature that adds*_now(clock)convenience methods.reliakit-retrynever sleeps or spawns; the caller injects any waiting, so it forces no async runtime. reliakit-deriveis a proc-macro crate. It runs at compile time on the host, so the usualno_std/allocdiscussion does not apply to it; the code it generates inherits theno_stdsupport of the trait crate.
reliakit targets Rust 1.85, the minimum required by the 2024 edition. The MSRV is pinned at this floor, the lowest edition 2024 allows, so the crates remain usable as low-level dependencies. It is verified in CI.
Raising the MSRV is treated as a breaking change: it ships with a major version bump and is noted in the changelog. It is never raised silently in a patch release, so pinning a crate version keeps it building on the Rust it shipped with.
Contributions are welcome. New here? The pinned good first issues are a friendly place to start, and help wanted issues need a bit more design judgment. Please open an issue before submitting a pull request for non-trivial changes so the direction can be discussed first.
- Keep each crate minimal and focused.
- Add tests for any new public API surface.
- Run
cargo fmt,cargo clippy, andcargo testbefore submitting.
See CONTRIBUTING.md for guidelines, CHANGELOG.md
for release notes, RELEASING.md for the release process, and
SECURITY.md for vulnerability reporting.
Licensed under the MIT License. See LICENSE.
