Conversation
The runtime 429 backoff was purely exponential, so multiple clients or parallel workers could retry in lockstep and create avoidable bursts. Add bounded jitter when a new backoff window is created, while keeping duplicate 429s within the dedup window stable so the same retry window is reused instead of re-randomized.
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 8 minutes and 35 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| const backoffDelay = Math.min(baseDelay * Math.pow(2, attempt - 1), MAX_BACKOFF_MS); | ||
| const jitteredDelay = Math.min(addBackoffJitter(backoffDelay), MAX_BACKOFF_MS); | ||
| const delayMs = Math.max(baseDelay, jitteredDelay); |
There was a problem hiding this comment.
negative jitter silently discarded on attempt=1
for attempt=1, backoffDelay === baseDelay. if Math.random() returns a value below 0.5, jitteredDelay < baseDelay, and the Math.max floor restores it to baseDelay — so downward jitter has no effect on the first 429. the effective spread is [baseDelay, baseDelay×1.2] (one-sided upward), not the symmetric ±20% the constant implies.
this is the most common 429 case, so the anti-thundering-herd goal stated in the PR description is only half-realised for first retries. attempt=2+ are unaffected because backoffDelay >> baseDelay and symmetric jitter applies.
if the Math.max floor is intentional (to honour the server-sent Retry-After as a hard minimum), add a comment:
| const backoffDelay = Math.min(baseDelay * Math.pow(2, attempt - 1), MAX_BACKOFF_MS); | |
| const jitteredDelay = Math.min(addBackoffJitter(backoffDelay), MAX_BACKOFF_MS); | |
| const delayMs = Math.max(baseDelay, jitteredDelay); | |
| const backoffDelay = Math.min(baseDelay * Math.pow(2, attempt - 1), MAX_BACKOFF_MS); | |
| const jitteredDelay = Math.min(addBackoffJitter(backoffDelay), MAX_BACKOFF_MS); | |
| // Math.max honours server Retry-After as a hard floor; on attempt=1 this makes jitter one-sided (upward only) | |
| const delayMs = Math.max(baseDelay, jitteredDelay); |
or remove the floor to get symmetric spread:
const delayMs = Math.max(0, jitteredDelay);Prompt To Fix With AI
This is a comment left during a code review.
Path: lib/request/rate-limit-backoff.ts
Line: 80-82
Comment:
**negative jitter silently discarded on attempt=1**
for attempt=1, `backoffDelay === baseDelay`. if `Math.random()` returns a value below `0.5`, `jitteredDelay < baseDelay`, and the `Math.max` floor restores it to `baseDelay` — so downward jitter has no effect on the first 429. the effective spread is `[baseDelay, baseDelay×1.2]` (one-sided upward), not the symmetric `±20%` the constant implies.
this is the most common 429 case, so the anti-thundering-herd goal stated in the PR description is only half-realised for first retries. attempt=2+ are unaffected because `backoffDelay >> baseDelay` and symmetric jitter applies.
if the `Math.max` floor is intentional (to honour the server-sent `Retry-After` as a hard minimum), add a comment:
```suggestion
const backoffDelay = Math.min(baseDelay * Math.pow(2, attempt - 1), MAX_BACKOFF_MS);
const jitteredDelay = Math.min(addBackoffJitter(backoffDelay), MAX_BACKOFF_MS);
// Math.max honours server Retry-After as a hard floor; on attempt=1 this makes jitter one-sided (upward only)
const delayMs = Math.max(baseDelay, jitteredDelay);
```
or remove the floor to get symmetric spread:
```typescript
const delayMs = Math.max(0, jitteredDelay);
```
How can I resolve this? If you propose a fix, please make it concise.|
Superseded by #387, which rebuilds the full open PR stack onto one reviewed integration branch. |
|
Closing in favor of #387. |
Problem
The runtime 429 backoff was purely exponential.
That means multiple clients or parallel workers can retry in lockstep after the same upstream rate limit window, creating avoidable bursts.
Fix
Add bounded jitter when a new rate-limit backoff window is created.
Duplicate 429s inside the dedup window continue to reuse the same stored delay so a single retry window stays stable instead of being re-randomized on every duplicate response.
Changes
lib/request/rate-limit-backoff.tstest/rate-limit-backoff.test.tsMath.random()to the neutral midpointValidation
note: greptile review for oc-chatgpt-multi-auth. cite files like
lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.Greptile Summary
this pr adds bounded ±20% jitter to the exponential rate-limit backoff in
lib/request/rate-limit-backoff.tsand stores the jittered delay on state so duplicate 429s within the dedup window return the same stable value instead of recomputing.key changes:
addBackoffJitter(baseMs)applies ±20% ofbaseMsusingMath.random() * 2 - 1RateLimitStategainslastDelayMsto cache the jittered delayprevious.lastDelayMsinstead of recalculatingMath.random()is globally mocked to0.5in tests for determinism; a new regression test covers jittered-first and stable-duplicate pathsissues found:
backoffDelay === baseDelayon the first 429, soMath.max(baseDelay, jitteredDelay)clamps any downward jitter to zero. the effective spread for attempt=1 is[baseDelay, baseDelay*1.2], not the symmetric ±20% the constant impliesMath.random()=0to confirm the floor behaviour is intentionalmockReturnValueOnce(1)tests an unreachableMath.random()value ([0,1)never includes 1); maximum achievable is ~0.9999getRateLimitBackoffWithReason, leaving the compounded effect of jitter +calculateBackoffMsuntestedConfidence Score: 3/5
safe to merge with low risk — logic change is narrow, 25 tests pass, but asymmetric attempt=1 jitter partially defeats the stated anti-thundering-herd goal and missing branches leave the floor behaviour undocumented
core backoff logic is correct, the dedup path is stable, and no concurrency issues are introduced (getRateLimitBackoff is synchronous, module-level Map is fine under Node.js single-thread model). score is 3 rather than 4 because the one-sided jitter on attempt=1 (the most common 429 case) quietly reduces spreading effectiveness, the missing vitest branches conflict with the project's 80%+ coverage requirement, and the test uses Math.random()=1 which is an unreachable value in production
lib/request/rate-limit-backoff.ts line 82 (Math.max floor asymmetry for attempt=1) and test/rate-limit-backoff.test.ts (missing attempt=1 negative-jitter branch and non-neutral jitter through getRateLimitBackoffWithReason)
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[getRateLimitBackoff called] --> B[pruneStaleRateLimitState] B --> C{previous state\nexists?} C -->|yes| D{now - lastAt\n< DEDUP_WINDOW 2s?} D -->|yes - isDuplicate| E[return previous.lastDelayMs\nisDuplicate: true] D -->|no| F{now - lastAt\n< RESET_MS 120s?} F -->|yes| G[attempt = consecutive429 + 1] F -->|no| H[attempt = 1] C -->|no| H G --> I[backoffDelay = min baseDelay × 2^attempt-1, MAX_60s] H --> I I --> J[addBackoffJitter\nbaseMs ± 20% via Math.random] J --> K[jitteredDelay = min jittered, MAX_60s] K --> L{jitteredDelay\n< baseDelay?} L -->|yes — suppressed on attempt=1| M[delayMs = baseDelay] L -->|no| N[delayMs = jitteredDelay] M --> O[store lastDelayMs in state] N --> O O --> P[return delayMs\nisDuplicate: false]Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "fix: add jitter to rate-limit backoff" | Re-trigger Greptile