cloudiful-rate-limiter 0.1.0

Reusable async resource throttling with local and Valkey-backed backends.
Documentation

cloudiful-rate-limiter

cloudiful-rate-limiter is an async throttling library for keyed resource access. It answers one question: can this key access a resource now, and if not, how long should the caller wait?

The published package name is cloudiful-rate-limiter, while the Rust library import is rate_limiter.

Links:

Version 0.1.0 exposes:

  • RateLimitPolicy::MinInterval for fixed minimum gaps between accesses
  • RateLimitPolicy::PerMinute for evenly spaced requests per minute
  • LocalRateLimiter for in-process throttling
  • optional ValkeyRateLimiter for shared throttling across instances
  • acquire, try_acquire, and peek APIs

Non-goals for this crate:

  • job scheduling
  • retries or backoff policies
  • token buckets
  • concurrency semaphores
  • HTTP middleware
  • business-specific key derivation

Add the crate

[dependencies]
rate_limiter = { package = "cloudiful-rate-limiter", version = "0.1.0" }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }

Enable shared Valkey backend:

[dependencies]
rate_limiter = { package = "cloudiful-rate-limiter", version = "0.1.0", features = ["valkey"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }

Core concepts

  • policy: throttling rule
  • key: explicit caller-provided scope such as eastmoney:quote
  • backend: state store implementation
  • acquire: consume one access slot, sleeping internally if needed
  • try_acquire: consume one slot only if currently allowed
  • peek: inspect without consuming a slot

This crate does not decide when a job starts. A scheduler, worker, CLI, or HTTP client controls that part.

Example: local minimum interval

use std::time::Duration;

use rate_limiter::{LocalRateLimiter, RateLimiter, RateLimitPolicy};

#[tokio::main]
async fn main() {
    let limiter = LocalRateLimiter::new(
        RateLimitPolicy::min_interval(Duration::from_millis(500)).unwrap(),
    )
    .unwrap();

    limiter.acquire("eastmoney:quote").await.unwrap();
    limiter.acquire("eastmoney:quote").await.unwrap();
}

Example: per-minute policy

use rate_limiter::RateLimitPolicy;
use std::time::Duration;

let interval = RateLimitPolicy::per_minute(120).unwrap().interval().unwrap();
assert_eq!(interval, Duration::from_millis(500));

Example: shared Valkey limiter

use std::time::Duration;

use rate_limiter::{RateLimiter, RateLimitPolicy, ValkeyRateLimiter};

#[tokio::main]
async fn main() {
    let limiter = ValkeyRateLimiter::new(
        "redis://127.0.0.1/",
        "market-data:",
        RateLimitPolicy::min_interval(Duration::from_secs(1)).unwrap(),
    )
    .await
    .unwrap();

    limiter.acquire("eastmoney:quote").await.unwrap();
}

Backend semantics

LocalRateLimiter uses process-local monotonic Instant.

ValkeyRateLimiter stores key -> next_allowed_timestamp_ms and uses one Lua script so competing instances observe atomic acquire decisions.

PerMinute currently maps to evenly spaced slots. It is not a token bucket.

Valkey integration tests

The Valkey integration tests are marked ignored so default CI stays hermetic. Run them explicitly with a reachable server:

RATE_LIMITER_VALKEY_URL=redis://127.0.0.1:6379/ cargo test --features valkey --test valkey_rate_limiter -- --ignored

In Gitea Actions, set the RATE_LIMITER_VALKEY_URL secret to enable the external Valkey integration test step in .gitea/workflows/ci.yml.