From f6f65e181b6050e0ab6e9fef6773e49e489ee3cf Mon Sep 17 00:00:00 2001 From: Deepanshu Date: Mon, 17 Aug 2026 12:12:40 -0400 Subject: [PATCH] fix(rate-limiting): reject key_ttl_seconds shorter than period_seconds A key_ttl_seconds override below period_seconds expired the bucket key before its window rolled over, resetting the counter to zero mid-window and letting tagged traffic exceed the configured limit. Only period_seconds and above is now accepted. --- litellm/types/router.py | 5 +++++ .../proxy/hooks/test_tag_rate_limiter.py | 13 ++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/litellm/types/router.py b/litellm/types/router.py index d8b2e457706..5c693354629 100644 --- a/litellm/types/router.py +++ b/litellm/types/router.py @@ -172,6 +172,11 @@ class TagRateLimitEntry(BaseModel): def _validate_key_ttl_seconds(self) -> "TagRateLimitEntry": if self.key_ttl_seconds is not None and self.key_ttl_seconds <= 0: raise ValueError("key_ttl_seconds must be a positive integer when set") + if self.key_ttl_seconds is not None and self.key_ttl_seconds < self.period_seconds: + raise ValueError( + "key_ttl_seconds must be at least period_seconds when set -- a shorter TTL expires the " + "counter before its window rolls over, letting tagged traffic reset to zero and exceed the limit" + ) return self @model_validator(mode="after") diff --git a/tests/test_litellm/proxy/hooks/test_tag_rate_limiter.py b/tests/test_litellm/proxy/hooks/test_tag_rate_limiter.py index 6617e0f9c45..dd547d88a4b 100644 --- a/tests/test_litellm/proxy/hooks/test_tag_rate_limiter.py +++ b/tests/test_litellm/proxy/hooks/test_tag_rate_limiter.py @@ -2461,7 +2461,7 @@ def test_ttl_for_concurrency_never_drops_below_the_safety_floor_even_with_a_lowe """ below_floor: Final = 10 assert ( - _PROXY_TagRateLimiter._ttl_for(_concurrency_limit(period_seconds=60, key_ttl_seconds=below_floor)) + _PROXY_TagRateLimiter._ttl_for(_concurrency_limit(period_seconds=5, key_ttl_seconds=below_floor)) == _CONCURRENCY_MIN_SAFETY_TTL_SECONDS ) @@ -2471,6 +2471,17 @@ def test_tag_rate_limit_entry_rejects_non_positive_key_ttl_seconds(): TagRateLimitEntry(name="per_minute", limit=1, period_seconds=60, key_ttl_seconds=0) +def test_tag_rate_limit_entry_rejects_key_ttl_seconds_shorter_than_period_seconds(): + """ + Regression test for a real bug: a key_ttl_seconds shorter than + period_seconds expires the bucket key before its window rolls over, + resetting the counter to zero mid-window and letting tagged traffic + exceed the configured limit. + """ + with pytest.raises(ValueError): + TagRateLimitEntry(name="per_minute", limit=1, period_seconds=60, key_ttl_seconds=59) + + # --------------------------------------------------------------------------- # per-tag max_in_memory_cache_size override -- dedicated cache partitions # ---------------------------------------------------------------------------