What happens
maxTotalTimeout is documented as a hard cap:
Maximum total time (in milliseconds) to wait for a response. If exceeded, an SdkError with code SdkErrorCode.RequestTimeout will be raised, regardless of progress notifications.
It is not enforced unless a progress notification arrives and resetTimeoutOnProgress is set. Without both, the budget does nothing.
Measured on main, with a transport that never responds:
{ timeout: 1000, maxTotalTimeout: 150 }
at 200ms → still pending (cap already exceeded)
at 1200ms → "Request timed out" (fired on `timeout`, not the cap)
{ maxTotalTimeout: 400 }
after 3000ms → still pending (waits for the 60s default timeout)
So a caller asking for a 150ms ceiling got 1000ms, and a caller asking for 400ms got 60s.
Why it happens
In packages/core-internal/src/shared/protocol.ts, _setupTimeout arms the pending timer with timeout only:
timeoutId: setTimeout(onTimeout, timeout),
maxTotalTimeout is read in exactly one place — the elapsed check inside _resetTimeout — and _resetTimeout has a single caller, guarded by:
if (timeoutInfo && responseHandler && timeoutInfo.resetTimeoutOnProgress) {
So the only way the budget is ever consulted is the progress path. A request with no progress notifications, or with resetTimeoutOnProgress left at its default of false, never checks it.
Why it survived
The existing test (test/shared/protocol.test.ts, "should respect maxTotalTimeout") passes resetTimeoutOnProgress: true and feeds two progress notifications, so it exercises the one path that works.
Expected
The pending timer should be armed for whichever limit comes first, and re-armed against the remaining budget on each progress notification, so the cap holds whether or not progress arrives.
Environment
main at 3924de99 (2.0.0-alpha.0). The same shape is present on v1.x.
Not related to #2224, #2136, #1870 or #2082 — those concern resetTimeoutOnProgress needing an onprogress callback, and none of them touch _setupTimeout or the arming.
I have a fix and tests ready and am happy to open a PR if useful.
AI assistance was used to investigate and write this report.
What happens
maxTotalTimeoutis documented as a hard cap:It is not enforced unless a progress notification arrives and
resetTimeoutOnProgressis set. Without both, the budget does nothing.Measured on
main, with a transport that never responds:So a caller asking for a 150ms ceiling got 1000ms, and a caller asking for 400ms got 60s.
Why it happens
In
packages/core-internal/src/shared/protocol.ts,_setupTimeoutarms the pending timer withtimeoutonly:maxTotalTimeoutis read in exactly one place — the elapsed check inside_resetTimeout— and_resetTimeouthas a single caller, guarded by:So the only way the budget is ever consulted is the progress path. A request with no progress notifications, or with
resetTimeoutOnProgressleft at its default offalse, never checks it.Why it survived
The existing test (
test/shared/protocol.test.ts, "should respect maxTotalTimeout") passesresetTimeoutOnProgress: trueand feeds two progress notifications, so it exercises the one path that works.Expected
The pending timer should be armed for whichever limit comes first, and re-armed against the remaining budget on each progress notification, so the cap holds whether or not progress arrives.
Environment
mainat3924de99(2.0.0-alpha.0). The same shape is present onv1.x.Not related to #2224, #2136, #1870 or #2082 — those concern
resetTimeoutOnProgressneeding anonprogresscallback, and none of them touch_setupTimeoutor the arming.I have a fix and tests ready and am happy to open a PR if useful.
AI assistance was used to investigate and write this report.