fix(admin): floor the silent-refresh delay so it can never hot-loop (#323) - #329
Merged
Conversation
…323) `msUntilRefresh` clamped to 0: Math.max(0, exp * 1000 - skewSeconds * 1000 - nowMs) Whenever a token's remaining life was shorter than the 60s skew, that returned 0, the scheduler called `setTimeout(…, 0)`, the refresh minted another equally short-lived token, and the next delay was 0 again. Not a retry — an unbounded tight loop. Measured against a dev stack with JWT_ACCESS_TTL=15: ~180 requests/second, 32,915 calls to /auth/refresh in a few minutes, every one returning 200, and single-use rotation churning a refresh-token row each time. Two changes, both about never scheduling zero: - MIN_REFRESH_DELAY_MS floor (5s). A pathological case is now slow instead of hot, whatever the arithmetic does. - The skew ADAPTS: `min(60s, remainingLife / 2)`. Reserving a fixed 60s of a 15s token is meaningless — it marks every refresh permanently overdue, which is what starts the loop. Half the remaining life always leaves real time on the clock. Production timing is UNCHANGED: a 900s token still refreshes at 840s. Only lifetimes at or under the skew window move. CORRECTION to the issue as filed: #323 claimed this bites in production ~15 minutes after sign-in at JWT_ACCESS_TTL=900. That is wrong. At 900s the scheduler fires once at 840s and reschedules normally — no loop. The loop requires a TTL at or below the 60s skew, which I had set myself while testing #318. It is a real robustness defect (any short TTL, clock skew, or a token already near expiry self-DoSes) but NOT the production incident described. The issue is being corrected and downgraded from P0. An existing test asserted `toBe(0)` for a 30s token — it encoded the buggy contract, so it is updated rather than worked around: that token now refreshes at 15s. Five regression tests cover the loop condition, the adaptive skew, an expired token and an unparseable one. 13 tests pass; type-check and lint green. Closes #323. Co-Authored-By: Claude Opus 5 <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.
msUntilRefreshclamped to0:Whenever a token's remaining life was shorter than the 60s skew that returned 0, the scheduler called
setTimeout(…, 0), the refresh minted another equally short-lived token, and the next delay was 0 again. Not a retry — an unbounded tight loop: ~180 req/s, 32,915 calls to/auth/refreshin a few minutes, every one returning 200, churning a refresh-token row each time via single-use rotation.Fix — two changes, both about never scheduling zero:
MIN_REFRESH_DELAY_MSfloor (5s) — a pathological case becomes slow rather than hot, whatever the arithmetic does.min(60s, remainingLife / 2). Reserving a fixed 60s of a 15s token marks every refresh permanently overdue, which is what starts the loop.Correction to the issue as filed
#323 claimed this bites in production ~15 minutes after sign-in at
JWT_ACCESS_TTL=900. That is wrong. At 900s the scheduler fires once at 840s and reschedules normally — no loop. It requires a TTL at or below the 60s skew, which I had set myself while testing #318. Still a real robustness defect, but not the production incident described — issue corrected and downgraded P0 → P2.Verification
toBe(0)for a 30s token — it encoded the buggy contract, so it is updated, not worked aroundCloses #323.
🤖 Generated with Claude Code