feat(middleware): Support multiple simultaneous rate limit conditions Closes #1262#4654
Open
Hossam-Ismail wants to merge 1 commit intolitestar-org:mainfrom
Open
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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