Skip to main content

sluice

Distributed rate limiter for Python: Token Bucket, Sliding Window Log, and Sliding Window Counter — Redis-backed, Lua-atomic, with a FastAPI/Starlette middleware and a circuit-breaker fallback for Redis outages.

Features

  • Three algorithms, each with a documented memory/accuracy/clock-skew trade-off — pick per-route based on what the endpoint actually needs
  • Atomic by construction — every check runs as a single Lua script (EVALSHA, cached via SCRIPT LOAD at connect time) so there's no check-then-act race between concurrent requests
  • Redis-authoritative time — scripts use the Redis server clock (TIME) instead of client clocks, so limiter correctness doesn't depend on NTP sync across app nodes
  • Circuit breaker with local fallback — a three-state breaker (CLOSED / OPEN / HALF_OPEN) that, on sustained Redis failures, degrades to a bounded, self-expiring in-process limiter (TTLCache) instead of failing open or failing closed
  • Declarative FastAPI middleware — a fluent RateLimitPolicy builder maps routes to algorithms; per-route Redis keys are auto-namespaced so two routes never share a counter by accident
  • Pluggable backend — RateLimitBackend is a Protocol (now_ms + evalsha); RedisBackend, FallbackBackend, and HybridBackend all implement it interchangeably

Installation

pip install sluice-limiter

Requires Python 3.11+.

Quick start

from fastapi import FastAPI
from sluice import RateLimitMiddleware, RateLimitPolicy
from sluice.backends import RedisBackend
from sluice.algorithms import SlidingWindowCounter, SlidingWindowLog, TokenBucket

backend = RedisBackend.from_url("redis://localhost:6379/0")

policy = (
    RateLimitPolicy(backend)
    # Auth: exact counting — a ±5% error on a brute-force target is a
    # security issue, not a trade-off.
    .limit("/api/login",  SlidingWindowLog,     limit=5,  window="1m")
    # Expensive endpoint: approximate is fine, memory matters at scale.
    .limit("/api/report", SlidingWindowCounter, limit=10, window="1m")
    # Everything else: token bucket for smooth throughput, allows bursts.
    .default(TokenBucket, capacity=100, window="1m")
)

app = FastAPI()

@app.on_event("startup")
async def startup():
    await backend.connect()

app.add_middleware(RateLimitMiddleware, resolver=policy.resolver())

Rejected requests get a 429 with RateLimit-* headers and a Retry-After; allowed requests get the same headers attached so clients can self-throttle proactively. See examples/basic_app.py for a complete app with lifespan-managed startup/shutdown.

Algorithms

Algorithm Memory Accuracy Clock-skew sensitivity Best for
Token Bucket O(1) — 2 fields/key Allows controlled bursts Benign (skew shifts refill slightly, self-corrects) General-purpose APIs, smooth throughput
Sliding Window Log O(N) — 1 entry per request in-window Exact Sensitive — skew delays pruning, slightly under-counts Security-sensitive limits (login, payment) where false positives are costly
Sliding Window Counter O(1) — 2 counters/key Approximate, bounded error (~<5% typical) Moderate — divergence > window_ms between nodes can create phantom capacity High-traffic endpoints where memory matters more than exactness

All three are exposed as async classes (TokenBucket, SlidingWindowLog, SlidingWindowCounter) sharing one interface:

result = await limiter.check(identifier, cost=1)
# result.allowed, result.remaining, result.reset_after_ms, result.retry_after_ms

identifier scopes the counter (typically f"{ip}:{path}"); cost lets a single call consume more than one unit (native support in Token Bucket, sequential admission for the window-based algorithms).

Handling Redis outages

A naive rate limiter has two bad options when Redis goes down: fail open (let everything through, right when abuse is most likely) or fail closed (take the service down). HybridBackend wraps RedisBackend in a circuit breaker that instead falls back to a local, per-node in-memory limiter:

from sluice.backends import HybridBackend

backend = HybridBackend(
    redis_url="redis://localhost:6379/0",
    failure_threshold=5,       # consecutive failures before opening the circuit
    recovery_timeout_sec=30.0, # base backoff before probing Redis again
    redis_call_timeout_sec=0.5,
)

While the circuit is OPEN, each node enforces limits independently — the effective cluster-wide limit becomes N × configured_limit for N running instances — which is a documented, deliberate trade-off rather than an oversight: legitimate traffic keeps flowing and each node still bounds abuse, and it self-corrects the moment Redis recovers (via a single HALF_OPEN probe, jittered so instances don't all re-probe in lockstep).

Declarative policies

RateLimitPolicy keeps all rate-limit configuration in one place instead of scattering decorators across route handlers, and auto-namespaces Redis keys per route so two TokenBucket rules never collide on the same default key prefix:

policy = (
    RateLimitPolicy(backend)
    .limit("/api/upload", TokenBucket, capacity=10, rate="5/min")
    .default(TokenBucket, capacity=100, window="1m")
    .identifier(jwt_subject_identifier)  # rate-limit per authenticated user instead of per IP
)

Built-in identifier functions: ip_identifier, ip_route_identifier (default), jwt_subject_identifier.

Project layout

sluice/
├── algorithms/     token_bucket.py, sliding_window_log.py, sliding_window_counter.py
├── backends/        redis_backend.py, fallback_backend.py, hybrid_backend.py
├── middleware/      fastapi.py, policy.py, utils.py
└── scripts/         token_bucket.lua, sliding_window_log.lua, sliding_window_counter.lua
  • docs/design/ — write-ups on the atomic-Lua approach, the Redis-fallback design, and declarative rate limiting
  • docs/tradeoffs/ — algorithm selection guide and a deep dive on clock skew
  • benchmarks/ — traffic-pattern simulations (uniform, burst, window-boundary hammer, Poisson) comparing all three algorithms; run with python -m benchmarks.main
  • tests/ — algorithm correctness, circuit breaker state transitions, fallback behavior, hybrid backend integration, concurrency, and policy building

Development

pip install -e ".[dev]"
pytest

Tests run against fakeredis by default, so no live Redis instance is required.

License

MIT

Release files for sluice-limiter 0.1.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sluice-limiter 0.1.3
File Size Uploaded
sluice_limiter-0.1.3.tar.gz 33.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for sluice-limiter 0.1.3
File Interpreter ABI Platform
sluice_limiter-0.1.3-py3-none-any.whl Python 3 none any Details

Total release size: 62.2 kB

Release files / sluice_limiter-0.1.3.tar.gz

Download URL sluice_limiter-0.1.3.tar.gz
Size 33.1 kB
Tags Source
SHA-256 checksum
How to use checksums
c59114c1149cc07724c5ab0823a86a5d4f8cce49abc293df2afe0723339d172f
BLAKE2b-256 checksum
How to use checksums
c6965f6f739222053fd85abc36b4b32be7a532fc18cb1e4ec608fc5e529c356d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.5

Release files / sluice_limiter-0.1.3-py3-none-any.whl

Download URL sluice_limiter-0.1.3-py3-none-any.whl
Size 29.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a98ac0fe42c9daeeb3769fed0b7abc3bee7246a2ca0617009c00bedd430fcef1
BLAKE2b-256 checksum
How to use checksums
2e43fd23b3e4ed10c1895801debb186c4f06fe0141123e56d13031a99bb8a3c4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.5

Release history Release notifications | RSS feed

This release

0.1.3 This release

2 release files

0.1.2

2 release files

0.1.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page