Describe the bug
Watch kills a working watch after 30 seconds, even while events are still coming in.
The relevant lines are src/watch.ts#L42-L44, with the timeout value at src/watch.ts#L11:
const controller = new AbortController();
const timeoutSignal = AbortSignal.timeout(this.requestTimeoutMs); // 30_000
const signal = AbortSignal.any([controller.signal, timeoutSignal]);
and that signal is passed to the fetch that streams the watch body, src/watch.ts#L63-L68.
AbortSignal.timeout starts counting the moment the watch starts, and fetch keeps that signal active for as long as it's reading the response body. So it fires 30 seconds in regardless of whether anything is wrong with the connection, and done is called with a TimeoutError.
I might be reading the intent wrong. #2367 added this 30 seconds as requestInit.timeout, which in node-fetch meant a timer that only fired when nothing was being received. I expected that meaning to have carried over, so a healthy watch would be left alone.
Client Version
2.0.0
Server Version
1.35 (GKE)
To Reproduce
Start a watch on any resource and keep the connection healthy. done is called after 30 seconds with TimeoutError: The operation was aborted due to timeout, and the connection is closed from the client side.
Against a real cluster, watching /api/v1/pods on a busy namespace set — the event counter is still climbing when the client hangs up:
0.26s watch started, stream is open
0.26s event ADDED argo-events/argo-events-controller-manager-...
0.35s event ADDED argo-rollouts/argo-rollouts-...
...
10.01s ... 329 events received so far
20.01s ... 340 events received so far
30.02s DONE TimeoutError: The operation was aborted due to timeout
30.02s ... 349 events received so far
40.02s ... 349 events received so far
...
80.03s ... 349 events received so far
Nothing on my side aborted it, and the event count is frozen from 30.02s on because there is no longer a watch.
I also checked it against a fake server that sends one event and then deliberately never ends the response, so that nothing except the client is able to close the connection:
0.01s server: watch connection opened
0.02s client: event ADDED p1
30.02s client: done(TimeoutError: The operation was aborted due to timeout)
30.02s server: connection closed by client
On 1.4.0 the same test keeps the connection for as long as I leave it running and done is never called — but that isn't the behaviour I'm asking for either, since 1.4.0 never notices a genuinely dead connection. I'm expecting something in between: leave a working connection alone, hang up on a silent one.
Expected behavior
A watch on a working connection stays open until the server closes it or I abort it myself.
Example Code
The done callback is what shows it:
const t0 = Date.now();
const at = () => ((Date.now() - t0) / 1000).toFixed(2) + 's';
const kc = new KubeConfig();
kc.loadFromDefault();
await new Watch(kc).watch(
'/api/v1/pods',
{},
(phase, obj) => console.log(at(), 'event', phase, obj.metadata.name),
// fires at ~30s with a TimeoutError, even on a connection that is fine
(err) => console.log(at(), 'done', err ? `${err.name}: ${err.message}` : '(no error)'),
);
Environment
- OS: macOS (host) and Linux (
node:24-alpine container)
- Node.js version: 26.7.0 against the real cluster, 24.19.0 for the fake-server runs
- Cloud runtime: none — plain process / container
Additional context
This also affects ListWatch. Its retry delay is only reset when an event arrives, in src/cache.ts#L267 inside watchHandler, while the delay itself is applied on every reconnect in src/cache.ts#L207-L215. So on a resource where nothing ever happens, the delay keeps doubling until it reaches MAX_RECONNECT_DELAY_MS. Time between watch connections, measured against the fake server with no events:
30s → 31s → 32s → 34s → 38s → 46s → 60s
It settles at roughly 30 seconds watching, then 30 seconds not watching. With an event every 10 seconds it stays at a steady 30 seconds. This matters for quiet resources — in my case a ListWatch over CRDs, which can go a long time with no events.
I'm happy to send a PR if this is real. For the timeout, I think it needs to become a timer that's reset each time a line is received and cleared when the watch finishes, rather than one that runs for the whole request — that's a few lines and wants a test, so I didn't want to guess at it here. Separately, resetting reconnectDelayMs once the connection is established, instead of when the first event arrives, would stop quiet resources from backing off.
Describe the bug
Watchkills a working watch after 30 seconds, even while events are still coming in.The relevant lines are
src/watch.ts#L42-L44, with the timeout value atsrc/watch.ts#L11:and that signal is passed to the fetch that streams the watch body,
src/watch.ts#L63-L68.AbortSignal.timeoutstarts counting the moment the watch starts, andfetchkeeps that signal active for as long as it's reading the response body. So it fires 30 seconds in regardless of whether anything is wrong with the connection, anddoneis called with aTimeoutError.I might be reading the intent wrong. #2367 added this 30 seconds as
requestInit.timeout, which in node-fetch meant a timer that only fired when nothing was being received. I expected that meaning to have carried over, so a healthy watch would be left alone.Client Version
2.0.0Server Version
1.35(GKE)To Reproduce
Start a watch on any resource and keep the connection healthy.
doneis called after 30 seconds withTimeoutError: The operation was aborted due to timeout, and the connection is closed from the client side.Against a real cluster, watching
/api/v1/podson a busy namespace set — the event counter is still climbing when the client hangs up:Nothing on my side aborted it, and the event count is frozen from 30.02s on because there is no longer a watch.
I also checked it against a fake server that sends one event and then deliberately never ends the response, so that nothing except the client is able to close the connection:
On
1.4.0the same test keeps the connection for as long as I leave it running anddoneis never called — but that isn't the behaviour I'm asking for either, since 1.4.0 never notices a genuinely dead connection. I'm expecting something in between: leave a working connection alone, hang up on a silent one.Expected behavior
A watch on a working connection stays open until the server closes it or I abort it myself.
Example Code
The
donecallback is what shows it:Environment
node:24-alpinecontainer)Additional context
This also affects
ListWatch. Its retry delay is only reset when an event arrives, insrc/cache.ts#L267insidewatchHandler, while the delay itself is applied on every reconnect insrc/cache.ts#L207-L215. So on a resource where nothing ever happens, the delay keeps doubling until it reachesMAX_RECONNECT_DELAY_MS. Time between watch connections, measured against the fake server with no events:It settles at roughly 30 seconds watching, then 30 seconds not watching. With an event every 10 seconds it stays at a steady 30 seconds. This matters for quiet resources — in my case a
ListWatchover CRDs, which can go a long time with no events.I'm happy to send a PR if this is real. For the timeout, I think it needs to become a timer that's reset each time a line is received and cleared when the watch finishes, rather than one that runs for the whole request — that's a few lines and wants a test, so I didn't want to guess at it here. Separately, resetting
reconnectDelayMsonce the connection is established, instead of when the first event arrives, would stop quiet resources from backing off.