Skip to content

feat(middleware): Support multiple simultaneous rate limit conditions Closes #1262#4654

Open
Hossam-Ismail wants to merge 1 commit intolitestar-org:mainfrom
Hossam-Ismail:feat/multi-condition-rate-limiting
Open

feat(middleware): Support multiple simultaneous rate limit conditions Closes #1262#4654
Hossam-Ismail wants to merge 1 commit intolitestar-org:mainfrom
Hossam-Ismail:feat/multi-condition-rate-limiting

Conversation

@Hossam-Ismail
Copy link
Copy Markdown

This PR adds support for configuring multiple simultaneous rate limit conditions in RateLimitConfig, resolving a long-standing feature request.
Previously only a single window was enforceable:
pythonRateLimitConfig(rate_limit=("minute", 100))
Now you can enforce combined policies — e.g. "10/second AND 200/minute AND 5000/hour" — and a 429 is returned as soon as any condition is breached:
pythonRateLimitConfig(rate_limits=[("second", 10), ("minute", 200), ("hour", 5000)])
The existing rate_limit field remains fully supported with no changes needed in calling code.

Changes
litestar/middleware/rate_limit.py

RateLimitConfig

Added rate_limits: list[tuple[DurationUnit, int]] | None field.
Made rate_limit optional (default None); post_init validates that exactly one of rate_limit/rate_limits is provided.
Added _all_rate_limits property that normalises both forms into a list[tuple[DurationUnit, int]].
Updated docstrings and the middleware property docstring with a multi-limit example.

RateLimitMiddleware

Replaced the self.max_requests / self.unit pair with self.rate_limits: list[tuple[DurationUnit, int]].
call now iterates over all rate limits, collecting (unit, max_requests, cache_object, key) tuples. It checks every condition before persisting any update, so a request violating a later window cannot consume quota from an earlier one.
Per-limit cache keys are namespaced by unit (e.g. RateLimitMiddleware::testclient::minute) so each window is tracked independently.
create_response_headers and create_send_wrapper accept optional max_requests / unit overrides; when serving response headers for a successful request the most restrictive limit (fewest remaining requests) is reported.
retrieve_cached_history and set_cached_history now take an explicit unit parameter instead of reading self.unit.

tests/unit/test_middleware/test_rate_limit_middleware.py

Updated two tests that used positional RateLimitConfig(("second", 10)) to use the keyword form RateLimitConfig(rate_limit=("second", 10)).
Added 6 new tests:

test_rate_limit_config_requires_at_least_one_limit — ValueError when no limit provided
test_rate_limit_config_rejects_both_fields — ValueError when both provided
test_multiple_rate_limits_passes_when_all_satisfied — all requests succeed within both windows
test_multiple_rate_limits_blocked_by_tighter_window — per-second limit triggers first
test_multiple_rate_limits_blocked_by_wider_window — per-minute limit triggers before per-second
test_rate_limits_all_rate_limits_property_single / _multi — property normalisation

Resolves litestar-org#1262

Previously `RateLimitConfig` only accepted a single `rate_limit` tuple,
making it impossible to enforce combined time-window policies such as
"10 req/second AND 200 req/minute AND 5000 req/hour".

Changes:
- Add `rate_limits: list[tuple[DurationUnit, int]] | None` field to
  `RateLimitConfig`.  When provided, every condition is evaluated on
  every request and the first breached limit triggers a 429.
- Keep `rate_limit` for full backward-compatibility; it is normalised
  to a single-element list via the new `_all_rate_limits` property.
- `RateLimitMiddleware` now stores a list of `(unit, max_requests)` pairs
  and checks each in order before updating any cache entry, so a request
  that violates a later condition does not consume quota from earlier ones.
- Per-limit cache keys are namespaced by unit (e.g. `::second`, `::minute`)
  so windows are tracked independently.
- Response headers reflect the most restrictive active limit (fewest
  remaining requests) when multiple conditions are configured.
- Add validation in `__post_init__`: raises `ValueError` when neither or
  both fields are provided simultaneously.
- Add six new unit tests covering: config validation, all-limits-satisfied,
  tight-window violation, wide-window violation, and `_all_rate_limits`
  normalisation for both single and multi-limit forms.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Hossam-Ismail Hossam-Ismail requested review from a team as code owners March 30, 2026 02:24
@github-actions github-actions bot added area/middleware This PR involves changes to the middleware size: small type/feat pr/external Triage Required 🏥 This requires triage labels Mar 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/middleware This PR involves changes to the middleware pr/external size: small Triage Required 🏥 This requires triage type/feat

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant