Skip to content

fix(whatsmeow): back off the reconnect loop instead of spinning forever - #197

Open
EcoosUP wants to merge 1 commit into
evolution-foundation:mainfrom
EcoosUP:fix/reconnect-loop-backoff
Open

fix(whatsmeow): back off the reconnect loop instead of spinning forever#197
EcoosUP wants to merge 1 commit into
evolution-foundation:mainfrom
EcoosUP:fix/reconnect-loop-backoff

Conversation

@EcoosUP

@EcoosUP EcoosUP commented Sep 10, 2026

Copy link
Copy Markdown

The problem

An instance whose device was logged out from the phone never stops restarting:

Disconnected → ReconnectClient → instance comes up with no session
   → QR → nobody scans → max QR count → forced logout → Disconnected → ...

Measured in production, from one instance after a Logged out for reason 401: logged out from another device:

reconnects in 50 minutes 110
QR codes generated 1135
stopped when a human noticed

Why nothing caught it

Nothing counts those restarts, because from ReconnectClient's point of view every turn succeeds — the instance really does come up. What fails afterwards is the pairing, and that was not being measured. A retry counter around the reconnect call would sit at zero the whole time.

The shape of the fix

The first 5 restarts inside a 15 minute window go straight through. That is the good case and it must stay fast: a healthy instance that lost its websocket has to be back in seconds, and that is the common event.

Past that, the loop becomes a growing wait — 5, then 15, then 30 minutes, with the last step repeating. The instance keeps trying: this is a backoff, not a give-up. An instance with a valid session still heals on its own; what is lost is the hammering, from roughly 2 restarts a minute to 2 an hour.

The counter is cleared on events.Connected, so a drop tomorrow does not inherit today's restarts.

One detail worth flagging

Only one goroutine waits per instance (scheduled). Without that guard, every Disconnected arriving during the wait would stack another waiting goroutine, and the backoff would become the very loop it was written to stop.

Relationship to the other PRs

This is independent of #194 and #196, but the three came out of the same incident. #194 is the one that actually took the deployment down; this one stops the condition that triggered it. They can be merged in any order.

Testing

go build ./... against main, clean. Running in production, adapted to a fork that also carries #194 and #196.

The thresholds are the ones that made sense for our load and are easy to disagree with — happy to make them configurable, or to change the numbers, if you prefer.

Summary by Sourcery

Add per-instance reconnect backoff to stop disconnected WhatsApp clients from hammering the restart loop while preserving rapid recovery for healthy sessions.

Bug Fixes:

  • Prevent disconnected instances from repeatedly restarting and generating QR codes indefinitely by applying progressive reconnect backoff.
  • Reset reconnect backoff after a successful connection.

Enhancements:

  • Limit reconnect handling to one scheduled wait per instance to prevent overlapping retry goroutines.
  • Allow the first five reconnects within a 15-minute window to proceed immediately before applying repeated 5-, 15-, and 30-minute delays.

An instance whose device was logged out from the phone never stops restarting:

    Disconnected -> ReconnectClient -> instance comes up with no session
    -> QR -> nobody scans -> max QR count -> forced logout -> Disconnected

Measured in production: 110 reconnects and 1135 QR codes in 50 minutes from a
single instance, stopping only when a human noticed.

Nothing counted those restarts, because from ReconnectClient's point of view
every turn succeeds — the instance really does come up. What fails afterwards is
the pairing, and nobody was measuring that.

The first 5 restarts inside a 15 minute window still go straight through: that
is the good case, a healthy instance that lost its websocket and has to return
within seconds. Past that the loop becomes a growing wait (5, 15, 30 minutes)
and the instance keeps trying — a backoff, not a give-up.

Only one goroutine waits per instance. Without that guard, every Disconnected
arriving during the wait would stack another one and the backoff would become
the loop it was written to stop.
@sourcery-ai

sourcery-ai Bot commented Sep 10, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR adds synchronized, per-instance reconnect-loop protection: normal reconnects remain immediate, but repeated disconnects enter a bounded repeating delay while concurrent disconnect events are coalesced. Successful connections clear the state so healthy instances recover quickly and do not inherit stale backoff.

Sequence diagram for per-instance reconnect backoff

sequenceDiagram
    participant WhatsApp
    participant Handler as myEventHandler
    participant Guard as reconnectAllowed
    participant Scheduler as ScheduledWait
    participant Service as ReconnectClient

    WhatsApp-->>Handler: events.Disconnected
    Handler->>Guard: reconnectAllowed(instanceID)
    alt first 5 restarts in 15 minutes
        Guard-->>Handler: true, 0
        Handler->>Service: ReconnectClient(instanceID)
    else backoff required
        Guard-->>Handler: false, wait
        Handler->>Scheduler: time.Sleep(wait)
        Scheduler->>Guard: reconnectWaitDone(instanceID)
        Scheduler->>Service: ReconnectClient(instanceID)
    else another wait is scheduled
        Guard-->>Handler: false, -1
        Handler-->>WhatsApp: skip duplicate reconnect
    end

    WhatsApp-->>Handler: events.Connected
    Handler->>Guard: reconnectSucceeded(instanceID)
    Guard-->>Handler: clear reconnect state
Loading

File-Level Changes

Change Details Files
Adds per-instance reconnect backoff state to prevent repeated post-logout restart and QR-generation loops from spinning indefinitely.
  • Tracks restart counts within a 15-minute window and permits the first five reconnects immediately.
  • Applies a 5/15/30-minute backoff ladder, repeating the final delay while continuing to retry.
  • Guards against multiple concurrent scheduled waits for the same instance.
  • Updates connection status with the backoff reason and logs duplicate, delayed, and failed reconnect paths.
pkg/whatsmeow/service/whatsmeow.go
Resets reconnect protection after a genuine WhatsApp connection is established.
  • Clears the instance's reconnect state on Connected or PushNameSetting events.
  • Allows future websocket drops to start with a fresh reconnect window.
pkg/whatsmeow/service/whatsmeow.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="pkg/whatsmeow/service/whatsmeow.go" line_range="346" />
<code_context>
+
+var (
+	reconnectMu    sync.Mutex
+	reconnectTrack = map[string]*reconnectState{}
+)
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Entries are added to the process-global `reconnectTrack` map on disconnects and are removed only after a `Connected` event. An instance that is deleted, manually stopped, or otherwise never reconnects leaves its state in the map indefinitely, so the map grows with stale per-instance entries for the lifetime of the process.

**Triggers:** When an instance is removed or stopped before emitting `events.Connected`.

**Suggested fix:** Delete the instance's reconnect state from every instance teardown/removal path, or add bounded cleanup for stale entries.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 1 finding to address first, and a wrong threshold or backoff schedule can leave a valid instance disconnected for up to 30 minutes and can cause availability loss before the change is reverted. Reverting stops future waits, but it cannot restore the connection time already missed.

Blocking findings: pkg/whatsmeow/service/whatsmeow.go:346


Sourcery is free for open source - if you like our reviews please consider sharing them ✨


var (
reconnectMu sync.Mutex
reconnectTrack = map[string]*reconnectState{}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Entries are added to the process-global reconnectTrack map on disconnects and are removed only after a Connected event. An instance that is deleted, manually stopped, or otherwise never reconnects leaves its state in the map indefinitely, so the map grows with stale per-instance entries for the lifetime of the process.

Triggers: When an instance is removed or stopped before emitting events.Connected.

Suggested fix: Delete the instance's reconnect state from every instance teardown/removal path, or add bounded cleanup for stale entries.

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