tower-rate-limiter
Keyed HTTP rate limiting middleware for Tower.
Each RateLimitLayer extracts a key from the request, resolves a quota, increments a Store, and
lets the rate-limit algorithm decide whether to call the ready inner service or return an immediate
HTTP response.
The crate is Tower-first. Axum and Redis are optional adapters.
Features
[]
= "0.1"
# Optional adapters and Redis execution properties
# tower-rate-limiter = { version = "0.1", default-features = false, features = ["redis", "runtime-smol"] }
| Feature | Provides |
|---|---|
redis |
The Redis Store adapter and Redis dependency |
redis-lua |
Use Lua instead of MULTI/EXEC for the atomic increment |
runtime-tokio |
Tokio-compatible Redis async runtime |
runtime-smol |
Smol-compatible Redis async runtime |
memory (default) |
Runtime-independent, process-local MemoryStore |
axum |
Support for reading Axum ConnectInfo<SocketAddr> in IpKeyExtractor |
redis, its increment implementation, and its async runtime are independent properties. A usable
Redis adapter combines redis or redis-lua with runtime-tokio or runtime-smol.
With --no-default-features, applications can provide their own Store, KeyExtractor, and
ResponseFactory without pulling in Axum, Redis, or Tokio.
Quick start
Start with tower_memory. It demonstrates a custom KeyExtractor,
an explicit MemoryStore, a fixed quota, and Tower Layer composition. For request-derived quotas
and downstream RateLimitContext, see tower_dynamic.
Builder defaults:
| Setting | Default |
|---|---|
| Limit | 1 |
| Window | 60 seconds |
| Policy name | default-policy |
| Store errors | Reject |
| RateLimit fields | Draft 11 |
The Store is always explicit via .with_store(...).
MemoryStore configures each cached entry to expire with its fixed window, so inactive keys are
eventually removed without a background task.
Request flow
Request
→ KeyExtractor
→ LimitProvider
→ Store::increment(key, window)
→ Usage { used, reset_after }
→ Allowed: call the inner service
→ RateLimited: return a response without calling the inner service
The core interfaces have narrow responsibilities:
KeyExtractorsynchronously returns an application-owned key from the request.LimitProviderasynchronously resolves the request's quota..limit(n)uses a fixedu64.Storeatomically increments a scoped key and returnsUsage.ResponseFactoryturns middleware outcomes into the application's response body and status.
LimitProvider finishes before the Store is called, so a provider failure consumes no quota. Once
both key and limit are available, the request is charged before the inner service is called and is
not refunded based on the inner response.
Fixed-window semantics
The first limit requests are allowed. Request limit + 1 is rate limited. Rejected requests
continue increasing Usage::used, but they do not extend the active window.
The first increment starts the window. Usage::reset_after is the remaining duration until that
window expires. Returning used == 0 violates the Store interface and follows the Store error
path.
The core scopes a key with the policy name before passing it to the Store. The window is passed separately. Layers that share a Store, policy name, and extracted key intentionally share usage; use different policy names for different policies or windows.
Errors and responses
Middleware failures use one closed RateLimitError type with three tuple variants:
Key(code, message)Quota(code, message)Store(code, message)
Each variant contains a stable machine-readable code followed by a diagnostic message. The
default ResponseFactory produces an empty response body with these statuses:
| Outcome | Status |
|---|---|
RateLimited |
429 Too Many Requests |
Key |
500 Internal Server Error |
Quota |
500 Internal Server Error |
Store |
503 Service Unavailable |
Applications can implement ResponseFactory to choose their own body, status, headers, and
logging. Store failures reject by default. Select StoreFailureMode::Allow through
RateLimitBuilder::store_failure_mode to call the inner service without claiming quota metadata when
the Store fails.
Key and limit failures never fail open.
Request bypass
Use RateLimitBuilder::skip to exempt requests using application-trusted request headers or
extensions. The predicate runs before client-key extraction and receives the request head with a
unit body:
use ;
let allowlist = new;
let limiter = builder
.skip
.with_store
.build?;
Bypassed requests call the inner service without extracting a client key, resolving a limit, charging the Store, or receiving rate-limit context or response fields. Keep proxy trust, authentication, and credential validation in the application; prefer checking a validated identity extension instead of matching raw credentials in this predicate.
RateLimit fields and context
By default, allowed and rate-limited responses include fields following the draft-11 definitions
of RateLimit-Policy
and RateLimit:
RateLimit-Policy: "<policy>";q=<limit>;w=<window-seconds>
RateLimit: "<policy>";r=<remaining>;t=<effective-window-seconds>
RateLimit-Policy advertises the configured fixed-window quota: q is the request limit and w
is the window in seconds. RateLimit reports the current service limit: r is the remaining
quota after the current request and t is the effective window in seconds, derived from the
Store's reset_after. The optional draft-11 qu (quota unit) and pk (partition key) parameters
are not emitted; omitting qu means the default quota unit is requests.
Select the older draft 7
format with .rate_limit_fields(RateLimitFields::Draft7):
RateLimit-Policy: <limit>;w=<window-seconds>
RateLimit: limit=<limit>, remaining=<remaining>, reset=<reset-seconds>
Draft 7 does not include the configured policy name in either field. Omit both fields with
.rate_limit_fields(RateLimitFields::Disabled); rate-limited responses still include
Retry-After. Active durations are rounded up to whole seconds for both revisions.
Allowed requests receive RateLimitContext in their request extensions. Its policies expose:
policy_name, limit, used, remaining, reset_after
Nested Layers append policies instead of overwriting existing context or response fields.
Axum
See axum_memory for ConnectInfo setup and nested policy scopes. For
a deployment-owned forwarding-header policy, see
axum_x_forwarded_for.
IpKeyExtractor reads a peer SocketAddr request extension and returns its IpAddr. With the
axum feature, it also reads ConnectInfo<SocketAddr>. It does not interpret forwarding headers
or define a trusted-proxy policy; applications own that policy.
Redis
RedisStore accepts an established redis::aio::MultiplexedConnection. It does not parse URLs,
open connections, or own connection shutdown.
See axum_redis for connection setup, namespacing, a shared Store, and
custom error responses.
Selecting redis without redis-lua uses one MULTI/EXEC transaction to initialize the counter
with SET NX PX, increment it, and return its PTTL. Adding redis-lua uses one Lua operation with
the same fixed-window semantics. Either implementation works with runtime-tokio or
runtime-smol. A missing or non-positive TTL is a Store error instead of an implicit repair. Redis
adds the rl: marker and the optional namespace to the key it receives.
Applications that need hashing or another representation can use
RateLimitBuilder::with_key_encoder to transform the scoped key before it reaches any Store. The
encoder must be deterministic, collision-resistant for the application's key space, non-blocking,
and free of I/O.
Examples
| Example | Shows |
|---|---|
tower_memory |
Basic Tower service with MemoryStore |
tower_dynamic |
Request-derived quota with LimitProvider |
axum_memory |
Application and route-scoped Axum policies |
axum_x_forwarded_for |
Application-owned forwarding-header trust policy |
axum_redis |
Shared Redis Store and custom error responses |
cargo run --example tower_memory --features memory
cargo run --example tower_dynamic --features memory
cargo run --example axum_memory --features axum,memory
cargo run --example axum_x_forwarded_for --features axum,memory
cargo run --example axum_redis --features axum,redis,runtime-tokio
Verification
cargo fmt --all -- --check
cargo test --no-default-features
cargo test --all-features
cargo clippy --all-targets --all-features -- -D warnings
cargo check --examples --all-features
cargo doc --all-features --no-deps
cargo package --allow-dirty --offline
Redis adapter unit tests live with RedisStore and cover transport-key formatting and increment
result parsing. The CI test job starts Redis and verifies all four runtime/implementation
combinations through the public Store interface. Local Redis integration tests require
REDIS_URL to point to a reachable test Redis server.
Scope
Version 0.1 intentionally focuses on fixed-window request limiting. Sliding windows, token buckets, weighted requests, refunds, Redis Cluster, Store lifecycle methods, and built-in forwarding-header trust are outside the current interface.