Actix Web

Backends

Choose where counts live (in process, in Redis, or both), tune the provider, and decide what happens when Redis fails.
BackendFeatureShared across instancesCost per request
LocalnoneNo: each process counts on its own.In memory.
RedisredisYes, exactly.One Redis round trip.
HybridredisYes, eventually: counts sync every interval.In memory for most requests.

Each backend builds its Trypema provider and remembers the window, so the limit advertised in headers always matches the one enforced. Build a backend once and clone it; clones share one provider.

Local

use actix_trypema::Local;
use trypema::{BucketSize, RateLimiterBuilder, WindowSize};

let backend = Local::new(WindowSize::seconds_or_panic(60))?;

// Any other provider option goes through Trypema's builder; the window is applied last.
let tuned = Local::configured(WindowSize::seconds_or_panic(60), |builder| {
    builder.bucket_size(BucketSize::milliseconds_or_panic(50))
})?;

Use Local for a single instance, or for limits that are fine per instance.

Redis

Every instance pointing at the same Redis and prefix shares one limit. Redis 7.2 or newer is required.

use actix_trypema::{Redis, TrypemaLimiter, extract::PeerIp};
use trypema::{RateLimit, RateLimiterBuilder, WindowSize, redis::RedisKey};

let connection = redis::Client::open("redis://127.0.0.1:6379/")?
    .get_connection_manager()
    .await?;

// Services sharing one Redis keep their counts apart with their own prefix.
let prefix = RedisKey::try_from("checkout-api")?;
let backend = Redis::configured(connection, WindowSize::minutes_or_panic(1), |builder| {
    builder.prefix(prefix)
})?;

let limiter = TrypemaLimiter::builder(backend)
    .namespace("ip")
    .extractor(PeerIp::default())
    .rate(RateLimit::per_minute_or_panic(60.0))
    .build()?;

The prefix separates services or environments sharing one Redis; the namespace separates limiters inside one service. Redis::new(connection, window) uses the default prefix.

Hybrid

Hybrid decides most requests from in-process state and syncs counts to Redis in the background. It shares limits across instances at close to local speed. The trade-off: an instance's view of the other instances lags by up to one sync interval, so the combined limit can briefly overshoot.

use actix_trypema::Hybrid;
use trypema::{WindowSize, hybrid::SyncInterval};

let connection = redis::Client::open("redis://127.0.0.1:6379/")?
    .get_connection_manager()
    .await?;

let backend = Hybrid::configured(connection, WindowSize::minutes_or_panic(1), |builder| {
    builder.sync_interval(SyncInterval::milliseconds_or_panic(50))
})?;

Most hybrid requests are decided without awaiting anything. Only requests that need Redis (a key's first request, a refresh, or a key whose local share ran out) take the async path. remaining_header(true) sends every request down the async path, so leave it off where speed matters.

When Redis fails

Every Redis and hybrid call is bounded by backend_timeout (200 ms by default). A timeout or an error is a backend failure, handled by on_backend_error:

PolicyOn failure
BackendErrorPolicy::FailOpen (default)Admit the request. A limiter outage should not become an API outage.
BackendErrorPolicy::FailClosedReject with 503 Service Unavailable. Use it on login, OTP and other abuse-prone routes.
BackendErrorPolicy::Fallback(local)Decide with a Local backend instead; each instance enforces the limit on its own.
BackendErrorPolicy::Custom(fn)Decide per failure.
use std::{sync::Arc, time::Duration};

use actix_trypema::{
    BackendErrorPolicy, BackendFailure, ErrorAction, Local, Redis, TrypemaLimiter,
    extract::PeerIp,
};
use actix_web::http::StatusCode;
use trypema::{RateLimit, WindowSize};

let window = WindowSize::minutes_or_panic(1);

// Login: never let attempts through unchecked.
let login = TrypemaLimiter::builder(Redis::new(connection.clone(), window)?)
    .namespace("login")
    .extractor(PeerIp::default())
    .rate(RateLimit::per_minute_or_panic(5.0))
    .on_backend_error(BackendErrorPolicy::FailClosed)
    .backend_timeout(Duration::from_millis(30))
    .build()?;

// API: keep limiting per instance while Redis is down. The fallback must use the same window.
let api = TrypemaLimiter::builder(Redis::new(connection.clone(), window)?)
    .namespace("api")
    .extractor(PeerIp::default())
    .rate(RateLimit::per_minute_or_panic(600.0))
    .on_backend_error(BackendErrorPolicy::Fallback(Local::new(window)?))
    .build()?;

// Search: admit on errors, but shed load when Redis is merely slow.
let search = TrypemaLimiter::builder(Redis::new(connection, window)?)
    .namespace("search")
    .extractor(PeerIp::default())
    .rate(RateLimit::per_minute_or_panic(120.0))
    .on_backend_error(BackendErrorPolicy::Custom(Arc::new(|failure| match failure {
        BackendFailure::Timeout(_) => ErrorAction::Reject {
            status: StatusCode::SERVICE_UNAVAILABLE,
        },
        _ => ErrorAction::Admit,
    })))
    .build()?;

With Fallback, n instances admit up to n times the limit while Redis is down, and those counts are not carried back when it recovers. Failures are logged as warnings at most once per second per process, with the namespace and the failure kind only.