Skip to content

Add Redis-backed fixed-window rate limiting (INCREX with Lua fallback)#20

Open
MSC72m wants to merge 3 commits into
redis:mainfrom
MSC72m:add-rate-limiting
Open

Add Redis-backed fixed-window rate limiting (INCREX with Lua fallback)#20
MSC72m wants to merge 3 commits into
redis:mainfrom
MSC72m:add-rate-limiting

Conversation

@MSC72m

@MSC72m MSC72m commented Jul 1, 2026

Copy link
Copy Markdown

Rate-limit FastAPI endpoints using Redis as the shared backend. Per request, atomically increment a per-client window counter via INCREX when available (Redis 8.8+) or a Lua script when it is not. Rejects excess requests with 429 Too Many Requests and a Retry-After header.

@app.get("/items", dependencies=[Depends(rate_limit(limit=100, window=60))])
async def get_items():
    return {"ok": True}

Design

  • FastAPI dependencyrate_limit() returns a Depends()-compatible callable (not a decorator or middleware), matching the cache() pattern
  • Default key — client IP + method + path via default_rate_limit_key_builder()
  • Custom key builder — sync or async callables accepted
  • Custom prefix — override redis:fastapi:ratelimit with the prefix= parameter
  • INCREX path — single execute_command("INCREX", ...) call; capability detected once per pool lifetime
  • Lua fallback — atomic GET/INCR/EXPIRE via execute_command("EVAL", ...) when INCREX is unsupported

Alternatives considered

  • register_script() / eval() — most idiomatic redis-py approach, but the stubs annotate them as self: Redis, making them uncallable on the AsyncRedis | AsyncRedisCluster union. Stricter linters (mypy, basedpyright) would flag this, so we chose execute_command() which works cleanly on both types.
  • EVALSHA + fallback — avoids sending the full script body on subsequent calls, but requires a try/except NOSCRIPTEVAL pattern. We chose to avoid fallback patterns entirely.
  • SCRIPT LOAD at 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: lupa

fakeredis imports lupa directly for Lua script execution. Without it, the Lua fallback tests would crash. Scoped to test only — 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.check span with attributes (key, limit, window, allowed, remaining, backend) and increments a redis_fastapi.rate_limit.requests counter.

Fail-open

Redis errors during the check are logged and swallowed; the request proceeds without rate limiting.

Tests

  • 17 unit tests — allow/block/combinatorics, expired window, sync endpoint, custom key builder, Lua fallback, INCREX path, fail-open, key prefix
  • 3 OTel tests — span on allowed/blocked, metric recording
  • 7 integration tests — full request→Redis→response cycle via real Redis (auto-skipped without one)

Open questions

  • Window unitwindow=60 is seconds, matching INCREX EX. Rename to window_seconds, keep as-is, or accept timedelta?
  • Response headers — currently Retry-After only. Add X-RateLimit-* on every response?
  • Query params in key — default key excludes them. Include sorted params?
  • Fail policy — currently fail-open (matching cache module). Prefer fail-closed for rate limiting?

Closes #15

MSC72m added 2 commits June 30, 2026 17:30
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
@jit-ci

jit-ci Bot commented Jul 1, 2026

Copy link
Copy Markdown

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.

@tishun

tishun commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

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.

@tishun

tishun commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

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.

@tishun tishun mentioned this pull request Jul 20, 2026
@MSC72m

MSC72m commented Jul 21, 2026

Copy link
Copy Markdown
Author

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!

@tishun

tishun commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Well the project is new, so there is no established process, but generally:

  • we will tend to keep the Github issues relevant and track there all feature development
  • we will assign issues to people so that people do not end up working on the same thing (unless by intention)
  • we will (generally) keep discussions in Discord (there is a dedicated channel for the fastapi-redis-sdk)

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Redis-backed rate limiting for FastAPI endpoints (INCREX-based window counter)

2 participants