Skip to content

fix(sio-client): prevent the reconnection delay from dropping to zero - #5582

Merged
darrachequesne merged 2 commits into
socketio:mainfrom
b84330808:fix-reconnect-backoff-overflow
Nov 5, 2026
Merged

darrachequesne merged 2 commits into
socketio:mainfrom
b84330808:fix-reconnect-backoff-overflow

Conversation

@b84330808

Copy link
Copy Markdown
Contributor

The kind of change this PR does introduce

  • a bug fix
  • a new feature
  • an update to the documentation
  • a code change that improves performance
  • other

Current behavior

The exponential part of the reconnection backoff (reconnectionDelay * 2^attempts, in lib/contrib/backo2.ts) overflows to Infinity at attempt 1015 with the default options (1000 * 2^1015 > Number.MAX_VALUE), which is after about 85 minutes of failed reconnection attempts (reconnectionAttempts defaults to Infinity).

From there, when the jitter subtracts the deviation, it computes Infinity - Infinity = NaN, and Math.min(NaN, max) | 0 is 0. So about half of the following reconnection attempts are scheduled with no delay instead of reconnectionDelayMax.

Reproduction:

import { Manager } from "socket.io-client";

const manager = new Manager("http://localhost:3000", { autoConnect: false });
manager.backoff.attempts = 1015; // same state as after ~85 minutes of failed attempts

for (let i = 0; i < 10; i++) {
  console.log(manager.backoff.duration()); // 5000 or 0 (0 about half of the time)
}

New behavior

The exponential part is capped at Number.MAX_VALUE before the jitter is applied. The delay then stays at reconnectionDelayMax, as it already does once the backoff has reached it (from the 5th attempt with the default options). Delays computed before the overflow are unchanged.

A test is added in test/connection.ts.

Other information (e.g. related issues)

I did not find an existing issue or pull request about this.

I used Claude Code to write this change.

The exponential part of the backoff (reconnectionDelay * 2^attempts)
overflows to Infinity after 1015 attempts, which is about 85 minutes of
failed reconnections with the default options. The jitter then computes
Infinity - Infinity = NaN, and NaN | 0 = 0, so about half of the next
reconnection attempts happen without any delay.

The value is now capped at Number.MAX_VALUE before the jitter is
applied, so the delay keeps being reconnectionDelayMax, as it was before
the overflow.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

@darrachequesne darrachequesne left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for your pull request. Please see my comments below.

var ms = this.ms * Math.pow(this.factor, this.attempts++);
// after many attempts, the value overflows to Infinity, and the jitter
// below would then compute Infinity - Infinity = NaN (so a delay of 0)
ms = Math.min(ms, Number.MAX_VALUE);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If jitter is enabled, I think that this might still overflow:

const ms = Number.MAX_VALUE;
const deviation = Math.floor(rand * jitter * ms);
ms + deviation; // Infinity

How about something like:

Backoff.prototype.duration = function(){
  var ms = this.ms * Math.pow(this.factor, this.attempts++);

- if (this.jitter) {
+ if (this.jitter && isFinite(ms)) {
    var rand =  Math.random();
    var deviation = Math.floor(rand * this.jitter * ms);
    ms = (Math.floor(rand * 10) & 1) == 0  ? ms - deviation : ms + deviation;
  }

  return Math.min(ms, this.max) | 0;
};

This way:

  • Infinity is properly handled
  • behavior of always returning max when attempts is sufficiently big is preserved

What do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, that reads better too. Switched to if (this.jitter && isFinite(ms)) in 272c275. With attempts = 1015 it now always returns 5000, and normal delays are unchanged.

Suggested in review: this also covers ms + deviation overflowing near
Number.MAX_VALUE, and keeps returning the max delay after many attempts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@darrachequesne
darrachequesne merged commit d1e8618 into socketio:main Nov 5, 2026
7 checks passed
@darrachequesne

Copy link
Copy Markdown
Member

Nice, thanks 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants