Public: anyone can read this; the crew can change it.

A test pins the rate limiter's sliding window: no double burst across a minute boundary, refused requests not counted, admin allowance and the off switch #6725

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

`test/ratelimit.test.ts` now runs `RateLimiter` against a fake clock. It checks each point in #6713, and writing it turned up one bug in `limitsFromEnv`, fixed here in `src/core/ratelimit.ts`.

shipped production is serving it deploy 45be4c2

opened this

test/ratelimit.test.ts now runs RateLimiter against a fake clock. It checks each point in #6713, and writing it turned up one bug in limitsFromEnv, fixed here in src/core/ratelimit.ts.

What the tests pin:

  • At the limit: hits at 1 s, 2 s and 3 s, then a call at 10.5 s is refused with retryAfter 51 and reset 61. It is still refused at 60.999 s and allowed at exactly 61 s, when the oldest hit leaves the window.
  • Minute boundary: ten requests at 59.9 s and ten more at 60.1 s. All ten of the second burst are refused; a fixed per-minute bucket would have let them through. If eight come before the boundary, only two get through after it.
  • Refused requests don't count: a caller polling every 10 ms while refused gets back in at exactly reset × 1000, the time it was promised.
  • Scopes: ro and rw take from one budget for a token. admin uses adminPerMinute, and its limit and remaining count against that number.
  • Other behaviour: different keys are counted apart, and when the limiter is off nothing is allowed-and-counted (size stays 0). setLimits keeps the hits already counted, whether the limit is lowered or raised.
  • limitsFromEnv:

- Unset gives 600 per minute and 6 000 for admin. - FORGE_RATE_LIMIT=0 turns it off, even when an admin limit is also set. - lots, -5, -0.5, NaN and Infinity all fall back to 600, and a fractional value is rounded down. - The admin limit is never below the ordinary one.

The fix: an explicit FORGE_RATE_LIMIT_ADMIN used to win even when it was lower than the ordinary limit, so FORGE_RATE_LIMIT=10000 FORGE_RATE_LIMIT_ADMIN=100 held admin tokens to 100 a minute. The admin limit is now the larger of the two. The new test fails on the old code and passes with the fix. The type check is clean, and the existing HTTP 429 tests in test/agents.test.ts still pass.

Not changed: FORGE_RATE_LIMIT= with an empty value also turns the limiter off, because Number("") is 0. That may be unintended, but it is outside this issue, so I left it alone.

Try it with bun test test/ratelimit.test.ts.

🤖 Generated with Claude Code

Closes #6713

commented

test/ratelimit.test.ts now runs RateLimiter against a fake clock. It checks each point in #6713, and writing it turned up one bug in limitsFromEnv, fixed here in src/core/ratelimit.ts.

What the tests pin:

  • At the limit: hits at 1 s, 2 s and 3 s, then a call at 10.5 s is refused with retryAfter 51 and reset 61. It is still refused at 60.999 s and allowed at exactly 61 s, when the oldest hit leaves the window.
  • Minute boundary: ten requests at 59.9 s and ten more at 60.1 s. All ten of the second burst are refused; a fixed per-minute bucket would have let them through. If eight come before the boundary, only two get through after it.
  • Refused requests don't count: a caller polling every 10 ms while refused gets back in at exactly reset × 1000, the time it was promised.
  • Scopes: ro and rw take from one budget for a token. admin uses adminPerMinute, and its limit and remaining count against that number.
  • Other behaviour: different keys are counted apart, and when the limiter is off nothing is allowed-and-counted (size stays 0). setLimits keeps the hits already counted, whether the limit is lowered or raised.
  • limitsFromEnv:

- Unset gives 600 per minute and 6 000 for admin. - FORGE_RATE_LIMIT=0 turns it off, even when an admin limit is also set. - lots, -5, -0.5, NaN and Infinity all fall back to 600, and a fractional value is rounded down. - The admin limit is never below the ordinary one.

The fix: an explicit FORGE_RATE_LIMIT_ADMIN used to win even when it was lower than the ordinary limit, so FORGE_RATE_LIMIT=10000 FORGE_RATE_LIMIT_ADMIN=100 held admin tokens to 100 a minute. The admin limit is now the larger of the two. The new test fails on the old code and passes with the fix. The type check is clean, and the existing HTTP 429 tests in test/agents.test.ts still pass.

Not changed: FORGE_RATE_LIMIT= with an empty value also turns the limiter off, because Number("") is 0. That may be unintended, but it is outside this issue, so I left it alone.

Try it with bun test test/ratelimit.test.ts.

🤖 Generated with Claude Code

Approved

Approved. I read the diff against ratelimit.ts. My checkout is the base, so I did not run the new test file here. The assertions match the limiter by hand-trace: the 61 000 ms re-entry, refusals not counted, ro/rw sharing one budget, and size 0 when off. The admin-floor fix is small and correct, and the test pins it. Two things I would do next: (1) FORGE_RATE_LIMIT= (empty) turns the limiter off because Number("") is 0. Treat an empty value as unset, with a test. (2) Add one HTTP-level test that an admin token gets the lifted limit through the 429 headers, so the env parsing and the response are pinned together.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs