April 3, 2026   -   David Oyinbo

distkit

Distkit (DISTributed systems KIT) is a Rust library of distributed systems primitives backed by Redis. It gives you four counter types (strict, lax, strict instance-aware, and lax instance-aware) for global consistency, it also includes sliding-window rate limiting via the trypema.

RustRedisDistributed SystemsLibraryAsync

Overview

Distkit ships four distributed counter implementations and re-exports a rate limiter (trypema).

Counter types

The counter types are what they sound like. They are used to hold counters across a distributed system. The counters are in two categories, the normal counters (StrictCounter, LaxCounter) and the instance aware counters. The normal counters behave like normal counters and can be useful for counting events across a shared key or namespace, while an instance aware counter keeps track of the counter each instance (or server) holds, if the instance dies, then the count it holds would be subtracted from the cumulative total. If the server is marked as dead because of, say, a network interruption, when the instance comes back online the values are recovered and synchronized with the global cumulative.

There is also a concept of epochs for the instance aware counters. An epoch is more like the version number of era the counters are in. If the epoch changes, it means we have moved on to start from zero. During recovery, an instance might attempt recovery, the recovery is only done if the last recorded epoch is still the same as the global epoch. So, recoveries are safe. Here is a table that highlights general features in each counter type:

StrictCounterLaxCounterStrictInstanceAwareCounterLaxInstanceAwareCounter
ConsistencyImmediateEventualImmediateEventual
inc latencyRedis round-tripSub-microsecondRedis round-tripSub-microsecond
Redis I/OEvery callBatchedEvery callBatched
Per-instance trackingNoNoYesYes
Dead-instance cleanupNoNoYesYes
Feature flagcounter (default)counter (default)instance-aware-counterinstance-aware-counter
Use caseBilling, inventoryHigh-throughput analyticsLive connection countsLive connection counts

StrictCounter

Every call is a single Redis round-trip executing an atomic Lua script. The stored value is always authoritative with no local state to go stale. Use this when accuracy is non-negotiable (billing, inventory, seat counts).

[dependencies]
distkit = "0.2"
use distkit::{RedisKey, counter::{StrictCounter, CounterOptions, CounterTrait}};

let client = redis::Client::open("redis://127.0.0.1/")?;
let conn = client.get_connection_manager().await?;
let prefix = RedisKey::try_from("my_app".to_string())?;
let counter = StrictCounter::new(CounterOptions::new(prefix, conn));

let key = RedisKey::try_from("orders".to_string())?;
counter.inc(&key, 1).await?;          // HINCRBY via Lua
counter.set(&key, 100).await?;        // HSET via Lua
let total = counter.get(&key).await?; // HGET
counter.del(&key).await?;             // HDEL, returns old value
counter.clear().await?;               // DEL on the hash

LaxCounter

When you can allow a small lag between writes and reads, use the LaxCounter. It would be significantly faster. The main caveat is that we get an eventual consistency instead of immediate consistency the StrictCounter offers.

use distkit::{RedisKey, counter::{LaxCounter, CounterOptions, CounterTrait}};

let counter = LaxCounter::new(CounterOptions::new(prefix, conn));

let key = RedisKey::try_from("impressions".to_string())?;
counter.inc(&key, 1).await?;         // local atomic add, sub-microsecond
let val = counter.get(&key).await?;  // reads local state, no Redis hit

Instance-aware counters

As mentioned above, the instance aware counters are a bit different from the normal counters. They track the counter on each instance and allow you to read the cumulative total. And they manage instance deaths and recovery.

[dependencies]
distkit = { version = "0.2", features = ["instance-aware-counter"] }

StrictInstanceAwareCounter

This is the StrictCounter with instance awareness. Every call is immediately consistent.

use distkit::icounter::{
    InstanceAwareCounterTrait,
    StrictInstanceAwareCounter, StrictInstanceAwareCounterOptions,
};
use distkit::RedisKey;

let counter = StrictInstanceAwareCounter::new(
    StrictInstanceAwareCounterOptions::new(prefix, conn),
);

let key = RedisKey::try_from("connections".to_string())?;

// All methods return (cumulative, this_instance_value).
let (total, mine) = counter.inc(&key, 5).await?;
let (total, mine) = counter.dec(&key, 2).await?;
let (total, mine) = counter.get(&key).await?;

// Adjust only this instance's slice (no epoch bump).
let (total, mine) = counter.set_on_instance(&key, 10).await?;

// Coordinate a global reset across all instances (bumps epoch).
let (total, mine) = counter.set(&key, 100).await?;

// Remove only this instance's contribution.
let (total, removed) = counter.del_on_instance(&key).await?;

// Delete the key globally and bump the epoch.
let (old_total, _) = counter.del(&key).await?;

LaxInstanceAwareCounter

The LaxInstanceAwareCounter is a buffered wrapper around StrictInstanceAwareCounter. It allowed for a small lag between writes and reads and batches the writes when necessary. This allowed for eventual consistency but at a high throughput since we are not paying the network price for each operation.

use distkit::icounter::{
    InstanceAwareCounterTrait,
    LaxInstanceAwareCounter, LaxInstanceAwareCounterOptions,
};
use distkit::RedisKey;
use std::time::Duration;

let counter = LaxInstanceAwareCounter::new(LaxInstanceAwareCounterOptions {
    prefix,
    connection_manager: conn,
    dead_instance_threshold_ms: 30_000,
    flush_interval: Duration::from_millis(20),
    allowed_lag:    Duration::from_millis(20),
});

let key = RedisKey::try_from("connections".to_string())?;

// Warm path: returns local estimate with no Redis call.
let (local_total, mine) = counter.inc(&key, 1).await?;
let (local_total, mine) = counter.dec(&key, 1).await?;
let (total, mine)       = counter.get(&key).await?;

Dead-instance cleanup

Each instance writes a liveness heartbeat to Redis on every flush. If a process silently dies, surviving instances detect the expired heartbeat and remove the orphaned contribution automatically the next time any of them touches the same key.

// Two separate processes / server instances:
let server_a = StrictInstanceAwareCounter::new(opts(conn1));
let server_b = StrictInstanceAwareCounter::new(opts(conn2));

server_a.inc(&key, 10).await?; // cumulative = 10
server_b.inc(&key,  5).await?; // cumulative = 15

// server_a goes offline.
// After dead_instance_threshold_ms (default 30 s), server_b's next call
// removes server_a's contribution automatically.
let (total, _) = server_b.get(&key).await?; // total = 5 once cleaned up

Rate limiting

Enable the trypema feature to access sliding-window rate limiting. distkit re-exports the entire trypema crate under distkit::trypema:

To get more details on trypema, you can see the project overview, the api documentation or the rust documentation

[dependencies]
distkit = { version = "0.2", features = ["trypema"] }

Three providers are available: local (in-process, sub-microsecond), Redis (distributed, atomic Lua enforcement), and hybrid (local fast-path with periodic Redis sync). Two strategies are available: absolute (binary allow/reject sliding window) and suppressed (probabilistic degradation that ramps rejection probability near capacity).

use distkit::trypema::{RateLimit, RateLimitDecision, RateLimiter, RateLimiterOptions};

// ... build rl: Arc<RateLimiter> at startup ...

let rate = RateLimit::try_from(10.0)?; // 10 requests per second

match rl.local().absolute().inc("user_123", &rate, 1) {
    RateLimitDecision::Allowed => { /* proceed */ }
    RateLimitDecision::Rejected { retry_after_ms, .. } => {
        eprintln!("Rate limited, retry in {retry_after_ms} ms");
    }
    _ => {}
}

Highlights

::div

class: "grid grid-cols-2 sm:grid-cols-4 gap-4 md:gap-6"

:::content-card

class: "col-span-2"

Architecture

::::div

class: "text-xs text-dune-600"

  • Four counter types across two feature flags
  • Strict variants: atomic Lua scripts on every call
  • Lax variants: in-memory DashMap buffer with background flush task
  • Instance-aware variants: UUID-namespaced Redis hash fields
  • All backed by redis::aio::ConnectionManager

:::: :::

:::content-card

class: "col-span-2"

Consistency model

::::div

class: "text-xs text-dune-600"

  • Strict counters: immediately consistent, no local state
  • Lax counters: eventual consistency, configurable lag (default 20 ms)
  • Instance-aware: per-key epoch prevents double-counting after set/del
  • set_on_instance adjusts only this instance's slice without touching the epoch

:::: :::

:::content-card

class: "col-span-2"

Warm-path performance

::::div

class: "text-xs text-dune-600"

  • LaxCounter::inc and LaxInstanceAwareCounter::inc: sub-microsecond, pure in-memory atomics
  • LaxInstanceAwareCounter::get and set_on_instance: also in-memory, no Redis round-trip
  • Flushes are batched into Redis pipelines on a configurable interval
  • Background flush task drops automatically when the counter is dropped

:::: :::

:::content-card

class: "col-span-2"

Dead-instance cleanup

::::div

class: "text-xs text-dune-600"

  • Each instance writes a heartbeat to <prefix>:liveness:<uuid> on every flush
  • Surviving instances scan liveness keys and remove expired contributions automatically
  • Configurable threshold via dead_instance_threshold_ms (default 30 s)
  • No manual cleanup needed; works correctly after crashes and ungraceful shutdowns

:::: :::

:::content-card

class: "col-span-2"

Safety & correctness

::::div

class: "text-xs text-dune-600"

  • #![forbid(unsafe_code)]: zero unsafe blocks in the library
  • No panics in library code; all fallible paths return DistkitError
  • 57 runnable doc-test examples, each isolated to a unique Redis key prefix
  • Criterion benchmarks for all four counter types across all operations

:::: :::

:::content-card

class: "col-span-2"

Feature flags

::::div

class: "text-xs text-dune-600"

  • counter (default): StrictCounter and LaxCounter
  • instance-aware-counter: StrictInstanceAwareCounter and LaxInstanceAwareCounter
  • trypema: sliding-window rate limiting via the trypema crate
  • All flags are independent and composable

:::: :::

::

Other Projects

October 1, 2021

Payaza Web SDK

A JavaScript Web SDK that simplifies integrating Payaza checkout on web applications. Built as part of my role at Payaza Africa.

SDKJavaScriptPayments
June 27, 2025

Actix Web Starter Template

Production-ready Rust/Actix Web REST API starter with RBAC auth, SeaORM/PostgreSQL, Kafka-based email, and Docker tooling.

RustActix WebSeaORM
May 10, 2024

Canvas Random Floating Circle

Pet project animating randomly floating circles on HTML Canvas with simple drift and easing.

JavaScriptHTML CanvasAnimation

Let's build something together

Available for senior engineering roles, consulting, and architecture reviews.

© 2026 David Oyinbo