fix(toolkit-lib): retry backoff has no jitter, retries stay in lockstep - #1942
Open
polothy wants to merge 1 commit into
Open
fix(toolkit-lib): retry backoff has no jitter, retries stay in lockstep#1942polothy wants to merge 1 commit into
polothy wants to merge 1 commit into
Conversation
`ConfiguredRetryStrategy` replaces smithy's own jittered backoff with whatever delay function it is handed, and `cappedExponentialBackoff` was deterministic. Requests throttled at the same moment therefore retried at exactly the same moments, so a herd of pollers stayed a herd instead of dispersing over the retry window, and burned its retry budget on re-collisions. Apply equal jitter - a random point in `[floor(d/2), d - 1]` - to the capped delay. The capped worst case is unchanged; no request waits longer than it does today. Refs aws#1777 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
polothy
requested a deployment
to
integ-approval
September 3, 2026 22:45 — with
GitHub Actions
Waiting
polothy
marked this pull request as ready for review
September 3, 2026 22:48
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.
Note
This PR was created by AI (Claude Code, Anthropic's agentic CLI), including the code, tests and this description. A human directed the work and reviewed the result, but the reasoning and wording below are the model's. Please review it on its merits rather than assuming human verification of every claim.
Related to #1777 (deliberately not using a closing keyword — that issue is about a different defect and should stay open). This is the secondary finding from its comment thread, and it stands alone: it reduces how often a request exhausts its retry budget under throttling, which is the precondition for the failure described there. It does not depend on, conflict with, or supersede #1941.
The problem
ConfiguredRetryStrategydoes not layer a custom delay onto the SDK's backoff — it replaces it:The strategy it displaces is jittered (
DefaultRetryBackoffStrategy, line 386).cappedExponentialBackoffwas not:So every request on a toolkit client that started retrying waited exactly 2s, 4s, 8s, 15s, 15s, 15s. Requests throttled in the same instant retry in the same instants: a herd stays a herd, re-colliding at each retry point rather than dispersing, and spends its budget on collisions with itself. Stack-event pollers are phase-aligned by construction (stacks in one deploy start together, nothing de-phases them), so this is the common case at high
--concurrency, not a corner case.The change
One function, in
packages/@aws-cdk/toolkit-lib/lib/api/aws-auth/sdk.ts:The cap is still applied before the jitter, so the documented worst case remains the cap.
Jitter applies only between retries. The first attempt of every request is unaffected, so a call that succeeds — or fails non-retryably — first time sees no change at all.
Each delay moves from a fixed value
dto a random point in[floor(d/2), d-1](equal jitter; the top isd-1rather thandbecause ofMath.floor). Every possible delay is therefore less than or equal to the delay before this change — no request can wait longer than it does today, and the capped worst case that #1500 established is preserved exactly.Both call sites of the helper are affected. The STS strategy (
sdk.ts:677) keeps its own inline schedule deliberately: it is a low-volume credential-validity probe that is not part of any throttling herd, and its 3-attempt budget already bounds it at 400ms.Base client config —
ConfiguredRetryStrategy(7, cappedExponentialBackoff(300, 15_000))Spread into every client the
SDKclass builds (S3, Lambda, SSM, ECR, CloudWatch Logs, Route 53, Secrets Manager, SFN, …).CloudFormation client —
ConfiguredRetryStrategy(7, cappedExponentialBackoff(1000, 15_000))The path in #1777.
Why equal jitter rather than the SDK's full jitter
Full jitter (
random * d, what smithy'sDefaultRetryBackoffStrategydoes and what #1777 proposed) would drop the CloudFormation mean to ~29.5s and allow a near-zero wait before the first retry. Equal jitter keeps the 59s worst case and a floor of half the scheduled delay, so the window a request has to ride out a throttling burst shrinks by 25% rather than 50%, while still dispersing simultaneous retries acrossd/2— 7.5s at the cap, roughly 75 request slots against the 10 req/sDescribeStackEventsquota.Switching to full jitter is a one-line change if reviewers prefer to match the SDK default.
How this compares to the SDK's own jitter
Same shape, one intentional deviation — smithy's default, verbatim:
[0, d)d/2[floor(d/2), d)0.75dBoth are exponential base-2, clamped,
Math.random()-scaled and floored; only the multiplier's range differs. Full jitter disperses better but halves the mean, which would shorten the CloudFormation budget from ~59s of wall clock to ~29.5s — the opposite of what helps when the problem is exhausting that budget. Equal jitter still spreads simultaneous retries acrossd/2(7.5s at the cap, ~75 request slots against the 10 req/sDescribeStackEventsquota) while keeping more of the window intact.Switching to full jitter is a one-line change if maintainers prefer to match the SDK default exactly.
One related observation, pre-existing and not addressed here:
StandardRetryStrategycallssetDelayBase(THROTTLING_RETRY_DELAY_BASE)(500ms, vs 100ms otherwise) when it classifies an error as throttling (retry/index.js:472). AcomputeNextBackoffDelaysupplied toConfiguredRetryStrategycloses over its own base and never readsthis.x, so that adaptation has always been inert for the toolkit. It matters less than it sounds — the CloudFormation client's 1000ms base already exceeds smithy's throttling base — but reviewers weighing this PR's throttling rationale should know the SDK's own throttling adaptation is not in play.Tests
test/api/aws-auth/sdk-retry.test.ts— the four existing tests keep their original expectations (2000,4000,8000,15_000,59_000) by mockingMath.random()to the top of the jitter window, so they still assert the schedule rather than jitter arithmetic. Three new tests cover the jitter itself: the floor at half the scheduled delay, a property check that 1,000 draws across attempts 1–10 all land in[floor(d/2), d), and that repeated draws for one attempt are not all identical.Mutation-checked by reverting the body to the deterministic version:
never waits less than half the scheduled delay,stays within the jitter window for every attemptanddisperses callers that retry at the same momentall fail. (The schedule tests pass under that mutation by design — mocking the top of the window makes them agree with the deterministic implementation, which is what keeps them readable.)The interval in the docstring was also verified against odd
baseMs/capMscombinations (333/15001,1/3,7/7,1001/9999), since this is a general helper andMath.floormakes the bounds inexact for odd delays.All seven
test/api/aws-authsuites pass (82 tests).eslintclean. No API Extractor change — the helper is exported only vialib/api/aws-auth/private.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license