Add Redis-backed fixed-window rate limiting (INCREX with Lua fallback)#20
Add Redis-backed fixed-window rate limiting (INCREX with Lua fallback)#20MSC72m wants to merge 3 commits into
Conversation
Adds rate_limit() as a FastAPI dependency with INCREX when available (Redis 8.8+) and atomic Lua fallback for older versions. - rate_limit(limit, window, key_builder=None, prefix=None) dependency - One-time INCREX capability detection cached in _PoolState - Retry-After header on 429 responses - Custom key builders (sync/async) and key prefix - OpenTelemetry span (rate_limit.check) and metric counter - 20 unit tests, 7 integration tests (auto-skipped without Redis) - Full docs page + README/mkdocs updates Closes redis#15
|
Hi, I’m Jit, a friendly security platform designed to help developers build secure applications from day zero with an MVS (Minimal viable security) mindset. In case there are security findings, they will be communicated to you as a comment inside the PR. Hope you’ll enjoy using Jit. Questions? Comments? Want to learn more? Get in touch with us. |
# Conflicts: # uv.lock
|
Hey @MSC72m , thanks for the contribution. The team needs some time to review the change and when we are ready we will write back here. |
|
Unfortunately we have already started work on the same feature and have made some considerable progress even before you opened the PR. I will try to incorporate some of your ideas and merge both PRs, but we need to do a considerable change. My bad about that, should have assigned #15 to a team member to indicate that. |
|
No worries at all, and thanks for the update! I completely understand how that can happen. I'm really glad to hear you'll be incorporating some of the ideas from my PR. I also had a look at your implementation and I really like the direction you're taking it. I'd definitely love to contribute more to the project going forward, and I'd like to avoid stepping on ongoing work like this if possible. Is the Issues tab the best place to find tasks to work on, or do you use another channel (Discord, Discussions, etc.) to coordinate who's working on what? Looking forward to contributing more! |
|
Well the project is new, so there is no established process, but generally:
However this does not mean we can't discuss larger topics here or propose new features in Discord. As a general rule of thumb - Discord is better for real-time chat, Github is better for offline / code-centric / spec-sentric discussions. |
Rate-limit FastAPI endpoints using Redis as the shared backend. Per request, atomically increment a per-client window counter via
INCREXwhen available (Redis 8.8+) or a Lua script when it is not. Rejects excess requests with429 Too Many Requestsand aRetry-Afterheader.Design
rate_limit()returns aDepends()-compatible callable (not a decorator or middleware), matching thecache()patterndefault_rate_limit_key_builder()redis:fastapi:ratelimitwith theprefix=parameterexecute_command("INCREX", ...)call; capability detected once per pool lifetimeexecute_command("EVAL", ...)when INCREX is unsupportedAlternatives considered
register_script()/eval()— most idiomatic redis-py approach, but the stubs annotate them asself: Redis, making them uncallable on theAsyncRedis | AsyncRedisClusterunion. Stricter linters (mypy, basedpyright) would flag this, so we choseexecute_command()which works cleanly on both types.EVALSHA+ fallback — avoids sending the full script body on subsequent calls, but requires a try/exceptNOSCRIPT→EVALpattern. We chose to avoid fallback patterns entirely.SCRIPT LOADat startup — would pre-cache the SHA per connection, but needs per-connection lifecycle hooks that are fragile across reconnects. Not worth it for a 14-line script.New test dependency:
lupafakeredisimportslupadirectly for Lua script execution. Without it, the Lua fallback tests would crash. Scoped totestonly — zero impact on production installs. If preferred, we can mock the Lua path to remove this dependency.OpenTelemetry
Every rate-limit check emits a
rate_limit.checkspan with attributes (key,limit,window,allowed,remaining,backend) and increments aredis_fastapi.rate_limit.requestscounter.Fail-open
Redis errors during the check are logged and swallowed; the request proceeds without rate limiting.
Tests
Open questions
window=60is seconds, matching INCREXEX. Rename towindow_seconds, keep as-is, or accepttimedelta?Retry-Afteronly. AddX-RateLimit-*on every response?Closes #15