Skip to content

Repository files navigation

Reliakit

English | Bahasa Indonesia

Reliakit

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.

CI codecov License: MIT MSRV: Rust 1.85 zero dependencies GitHub stars Last commit Ask DeepWiki

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.

Start here

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.

30-second example

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"

How it's different

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.

Why Reliakit?

  • 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 Port is always 1..=65535; a BoundedStr<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> / SecretString so they render as [REDACTED] in Debug, 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) and reliakit-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 reliakit umbrella for several; every block follows the same conventions and the same zero-dependency, no_std, no-unsafe rules. Reliability patterns usually mean stitching together unrelated crates with different designs and dependency trees; here they are built to fit.

Footprint

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, and cargo tree -p reliakit --all-features proves it.
  • No unsafe. Every crate declares #![forbid(unsafe_code)].
  • no_std-friendly. The core crates build for bare metal (for example thumbv7em-none-eabi); alloc and std are 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 reliakit umbrella behind per-crate feature flags.

Core features

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)

Which resilience block do I use?

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.

Real-world use cases

1. Backend / API input validation

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);

2. CLI tools / config parsing + secret-safe logging

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 it

3. Microservices / external calls: rate limiting and circuit breaking

Clock-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 service

4. Data pipelines / bounded input handling

use 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 });

5. Typed JSON for APIs and storage

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() });

6. Embedded / no_std-friendly constraints

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.

7. Protocols and deterministic encoding (incl. blockchain)

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.

8. Health and readiness endpoints / status pages

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.

9. Graded, explainable decisions (routing, selection, agents)

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.

Quick start / installation

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 overview

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.

Design philosophy

  • 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.

When to use Reliakit

  • 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_std code that needs constrained values or resilience math.

When not to use Reliakit

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/HttpUrl validation is pragmatic, not a full RFC implementation).

Feature flags & no_std

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 implies alloc. Building with --no-default-features gives the no_std subset.
  • Allocation-backed APIs need alloc. Owned types (String/Vec-backed, e.g. Email, BoundedStr, SecretString, BoundedVec, all of reliakit-json and reliakit-csv) require the alloc feature; 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, and reliakit-core need no allocation at all. circuit, ratelimit, and timeout offer an optional core feature that adds *_now(clock) convenience methods. reliakit-retry never sleeps or spawns; the caller injects any waiting, so it forces no async runtime.
  • reliakit-derive is a proc-macro crate. It runs at compile time on the host, so the usual no_std/alloc discussion does not apply to it; the code it generates inherits the no_std support of the trait crate.

Minimum Supported Rust Version (MSRV)

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.

Contributing

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, and cargo test before submitting.

See CONTRIBUTING.md for guidelines, CHANGELOG.md for release notes, RELEASING.md for the release process, and SECURITY.md for vulnerability reporting.

Star History

Star History Chart

License

Licensed under the MIT License. See LICENSE.

About

Reliability toolkit for Rust: validated types, secret redaction, bounded collections, canonical encoding, and runtime-agnostic resilience (backoff, circuit breaker, rate limiter). no_std, zero-dependency.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

11 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages