Repository navigation
feat(realtime): honour capacity retry_after, add capacityRetryBudgetMs and onCapacityWait - #221
Merged
Merged
Conversation
commit: |
…etryBudgetMs and onCapacityWait When the server refuses a dial for lack of capacity it now sends error_type "capacity" and retry_after (seconds). connect() hands that refusal out of p-retry and waits retry_after (±20 % jitter, at least 1 s) before dialling again, for as long as the new capacityRetryBudgetMs option allows (default 60 s since the first dial). Those waits are bounded by the budget, not by retries, but retries: 0 still dials once. onCapacityWait reports each wait to the app. Servers that send neither field keep today's backoff.
AdirAmsalem
force-pushed
the
conductor/sdk-capacity-retry-after
branch
from
October 8, 2026 12:38
0b666a2 to
d64c091
Compare
AdirAmsalem
marked this pull request as ready for review
October 8, 2026 14:44
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.
Problem
When the realtime pool is full, the server refuses the dial with close 1013 "Try Again Later" and the SDK re-dialled on p-retry's exponential backoff (1 s, 2 s, 4 s, 8 s, 10 s, then give up). It neither waited as long as the server asked nor kept trying for as long as a slot was likely to free up, and the app had no signal that a slow
connect()was a capacity wait.The server now tells capacity-refused clients when to come back. The refusal is one
errormessage, then the unchanged close:{"type": "error", "error": "Server at capacity. Please try again later.", "error_type": "capacity", "retry_after": 5}retry_afteris in seconds and may be fractional. Only the capacity refusal carries these fields; every other refusal is matched on its text and close reason exactly as before.Changes
ErrorMessagegains optionalerror_typeandretry_after;SignalingChannelcopies them onto theServerErrorthat rejects the join (errorType,retryAfter).StreamSession.connect()wraps the existingpRetry(...)call in a small loop.shouldRetryhas one new early return: a capacity refusal with a usableretry_afteris handed straight back (p-retry still tears the socket down), and the loop waitsretry_after±20 % jitter, floored at 1 s, then dials again. The 1 s floor keeps a tinyretry_afterfrom re-dialling into the refused socket's own close event. Everything else p-retry did is untouched: a capacity close or error withouterror_typenever enters the loop and keeps today's backoff; terminal and permanent refusals are unchanged; the reconnect after an established session drops is unchanged and does not readretry_after.capacityRetryBudgetMs(default 60 000): how long after its first dial aconnect()keeps re-dialling capacity refusals. A wait is only taken if it would end within the budget; after thatconnect()rejects with the server's own error (message,errorType,retryAfter), which still matches the capacity-refusal regex pinned by the existing tests.retriesand the budget: capacity waits are bounded by the budget alone and do not count againstretries;retries: 0turns them off as well, so it still dials exactly once. (Simpler than also capping waits by an explicitretries: N, which the budget already bounds.)onCapacityWait, called with{ retryAfterMs, attempt }before each wait (attemptcounts the waits of thisconnect()from 1). It is a connect option rather than a client event because it can only fire whileconnect()is pending, before the client object exists. New exportedCapacityWaittype.Tests
Five new cases in the existing harnesses:
StreamSession: a capacity error withretry_after: 5emitscapacityWaitonce and re-dials after 4 to 6 s (not at the 1 s backoff floor), and the re-dial connects;capacityRetryBudgetMs: 7_000stops after the first wait and rejects with the server's error;connectRetries: 0dials once and never waits.realtime.connect options:onCapacityWaitfires with the jittered wait and the re-dial reuses the same URL.SignalingChannel:error_type/retry_afterland on the rejecting error.Old-server behaviour (1013 "Try Again Later" without fields keeps the backoff) and the terminal session limit are covered by the existing tests, which are unchanged and still pass.
Verification
Run from
packages/sdk:tsdown(build)node tests/built-package.mjsbiome lintbiome checktsc --noEmitvitest unit --runCompatibility
Additive and backward compatible: no new required options, and servers that send neither field get exactly today's behaviour. No version bump or release in this PR.
Note
Medium Risk
Changes realtime connect retry orchestration (handshake path), but behavior is additive, backward compatible, and covered by new unit tests.
Overview
Realtime
connect()now treats server capacity refusals (error_type: "capacity"plusretry_afteron the signalingerrormessage) differently from generic 1013 "Try Again Later" closes: it waits the server’s delay (±20% jitter, at least 1s), re-dials withincapacityRetryBudgetMs(default 60s), and surfaces each wait via optionalonCapacityWait({ retryAfterMs, attempt }).retries: 0still means a single dial and also disables these capacity re-dials.SignalingChannelcopieserror_type/retry_afteronto join failures;StreamSessionwraps the existingp-retryloop so capacity errors bypass exponential backoff but stay bounded by the budget. Servers that omit the new fields keep today’s backoff behavior. README and exportedCapacityWaittype document the options.Reviewed by Cursor Bugbot for commit d64c091. Bugbot is set up for automated code reviews on this repo. Configure here.