Skip to content

fix(whatsmeow): reuse pooled authDB connection in StartClient instead of leaking a new pool per call - #174

Open
fmedeiros95 wants to merge 2 commits into
evolution-foundation:mainfrom
fmedeiros95:fix/whatsmeow-sqlstore-connection-leak
Open

fix(whatsmeow): reuse pooled authDB connection in StartClient instead of leaking a new pool per call#174
fmedeiros95 wants to merge 2 commits into
evolution-foundation:mainfrom
fmedeiros95:fix/whatsmeow-sqlstore-connection-leak

Conversation

@fmedeiros95

@fmedeiros95 fmedeiros95 commented Aug 14, 2026

Copy link
Copy Markdown

Problem

StartClient (pkg/whatsmeow/service/whatsmeow.go) opens a brand new *sql.DB connection pool on every call, via:

container, err = sqlstore.New(context.Background(), "postgres", w.config.PostgresAuthDB, dbLog)

sqlstore.New internally does sql.Open(dialect, address) — a fresh, completely unbounded pool (no SetMaxOpenConns/SetMaxIdleConns/SetConnMaxLifetime), independent from the properly-configured GORM pool set up in pkg/config/config.go. The container variable is local to StartClient and is never closed (container.Close() is not called anywhere in the file), so every call leaks the entire underlying connection pool.

StartClient is called repeatedly during normal operation, not just once per process lifetime:

  • on the LoggedOut event auto-restart (whatsmeow.go, "restart client")
  • on CONNECT_ON_STARTUP
  • when updating settings of an already-running instance
  • on manual reconnect (POST /instance/connect) — note this path only calls client.Disconnect() on the old whatsmeow.Client, it never closes the old sqlstore.Container/pool before creating a new one

In production, with only a couple of WhatsApp instances reconnecting periodically (WhatsApp forces session logout for various reasons on its own, no user action required), this accumulated ~95 idle Postgres connections over about 2 days and exhausted max_connections, taking down the whole app with remaining connection slots are reserved for roles with the SUPERUSER attribute — including breaking evolution-go's own ability to open new connections, so instances got stuck in a reconnect-fail loop.

Fix

whatsmeowService already holds authDB *sql.DB — the same GORM-backed, properly pool-limited connection injected via NewWhatsmeowService. sqlstore exposes NewWithDB(db *sql.DB, dialect string, log) exactly for reusing an existing connection instead of opening a new one. This PR switches the Postgres branches of StartClient to:

container = sqlstore.NewWithDB(w.authDB, "postgres", dbLog)
err = container.Upgrade(context.Background())

NewWithDB doesn't run Upgrade automatically like New does, so the Upgrade call is made explicitly right after, preserving the original behavior (schema stays up to date) while reusing the bounded pool instead of leaking a new one per call.

The SQLite branches are untouched — w.sqliteDB wraps a different file (dbdata/users.db vs. the whatsmeow store's dbdata/main.db), so blindly reusing it there would point at the wrong database; fixing that path would need separate investigation and is out of scope for this fix.

Testing

  • CGO_ENABLED=1 go build ./cmd/evolution-go — builds cleanly.
  • Deployed the patched binary to a production instance (2 workspaces, Postgres-backed POSTGRES_AUTH_DB, max_connections=100 shared with other apps on the same server). Before the fix, pg_stat_activity showed the evolution role accumulating idle connections on every instance reconnect/restart, with recurring Failed to create container: failed to upgrade database: ... remaining connection slots are reserved for roles with the SUPERUSER attribute in the logs. After deploying the fix and restarting, idle connections for the evolution role stayed at 1-2 across multiple reconnect cycles, with no recurrence of the pool-exhaustion error.

Summary by Sourcery

Bug Fixes:

  • Fix Postgres connection pool leaks caused by StartClient creating a new unbounded sql.DB on each invocation instead of reusing the shared authDB pool.

sqlstore.New() abria um *sql.DB novo, sem limites de pool e nunca
fechado, toda vez que StartClient rodava (conexao inicial, reconexao
manual, restart apos LoggedOut, loop de startup). Em producao isso
vazou o max_connections do Postgres apos ~2 dias com poucas dezenas
de instancias reconectando.

Troca por sqlstore.NewWithDB(w.authDB, ...), reaproveitando o pool ja
existente e limitado (SetMaxOpenConns/SetMaxIdleConns/etc, configurado
em pkg/config/config.go). Como NewWithDB nao roda Upgrade sozinho como
o New(), a chamada de Upgrade(ctx) foi movida para o call site.
@sourcery-ai

sourcery-ai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

This PR changes StartClient to reuse the existing, bounded Postgres authDB connection pool instead of opening and leaking a new *sql.DB pool on every call, while preserving the automatic schema upgrade behavior; SQLite handling is left unchanged.

Sequence diagram for StartClient Postgres container initialization

sequenceDiagram
    participant caller
    participant whatsmeowService
    participant sqlstore
    participant Postgres

    caller->>whatsmeowService: StartClient(cd)
    alt PostgresAuthDB configured
        whatsmeowService->>sqlstore: NewWithDB(authDB, postgres, dbLog)
        sqlstore-->>whatsmeowService: container
        whatsmeowService->>sqlstore: container.Upgrade(context.Background)
        sqlstore->>Postgres: apply schema migrations using authDB pool
        sqlstore-->>whatsmeowService: upgrade result
    else SQLite
        whatsmeowService->>sqlstore: New(context.Background, sqlite, dsn, dbLog)
        sqlstore-->>whatsmeowService: container
    end
Loading

File-Level Changes

Change Details Files
Reuse the existing Postgres authDB *sql.DB pool in StartClient via sqlstore.NewWithDB and explicitly run Upgrade, fixing a severe connection leak.
  • Replace sqlstore.New(...) Postgres calls in StartClient with sqlstore.NewWithDB(w.authDB, "postgres", ...) so that the existing GORM-managed pool is used.
  • Add explicit container.Upgrade(context.Background()) calls after NewWithDB because NewWithDB does not auto-run schema upgrades.
  • Keep SQLite branches as-is, continuing to open a dedicated sqlstore container using a file-based DSN for dbdata/main.db.
pkg/whatsmeow/service/whatsmeow.go

Possibly linked issues

  • #: PR replaces sqlstore.New with NewWithDB in StartClient, reusing the shared Postgres pool and stopping leaks.
  • #(unknown): The PR changes StartClient to use sqlstore.NewWithDB(authDB), fixing the Postgres connection pool leak described.
  • #(unknown): PR changes StartClient to use NewWithDB on w.authDB, fixing the Postgres pool leak described in the issue.

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 left some high level feedback:

  • Consider guarding the Postgres branches with a nil check on w.authDB so that NewWithDB doesn’t panic if the shared connection wasn’t initialized as expected.
  • The explanatory comment added above the Postgres NewWithDB call is quite long and in Portuguese; you might shorten it and align the language with the rest of the codebase for consistency.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider guarding the Postgres branches with a nil check on `w.authDB` so that `NewWithDB` doesn’t panic if the shared connection wasn’t initialized as expected.
- The explanatory comment added above the Postgres `NewWithDB` call is quite long and in Portuguese; you might shorten it and align the language with the rest of the codebase for consistency.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

- Bail out early with a clear log if PostgresAuthDB is configured but
  authDB was never initialized, instead of letting NewWithDB receive
  a nil *sql.DB.
- Shorten the explanatory comment above NewWithDB and switch it to
  English to match the rest of the codebase.
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.

1 participant