Skip to content

Periodic standby status update ignores auto: false, silently advancing replication slot position #174

Description

@holomekc

Description:

When configuring { auto: false, timeoutSeconds: 10 }, the expectation is that WAL position is only advanced via explicit acknowledge(lsn) calls. However, the checkStandbyStatus timer calls acknowledge(this._lastLsn) directly — bypassing the auto guard — which sends a standby status update with the last received LSN to PostgreSQL, advancing confirmed_flush_lsn.

This causes silent data loss in manual-ack workflows: if a message fails processing and is not explicitly acknowledged, a subsequent timer-driven standby status update will advance the pointer past it.

Root cause:

In checkStandbyStatus:

this.checkStandbyStatusTimer = setInterval(async () => {
    if (this._stop) return;
    if (this._lastLsn && Date.now() - this.lastStandbyStatusUpdatedTime > ...)
        await this.acknowledge(this._lastLsn);  // calls public acknowledge(), not _acknowledge()
}, 1000);

The private _acknowledge() correctly checks auto:

private async _acknowledge(lsn: string) {
    if (!this.config.acknowledge!.auto) return;  // guarded
    ...
}

But checkStandbyStatus calls this.acknowledge() (public) directly, which has no auto check.

Additionally, _lastLsn is updated on every received message unconditionally (including messages the user hasn't processed yet):

this._lastLsn = lsn;  // updated for every copyData, regardless of ack

###History:

We noticed that this exact issue was already identified and fixed in commit 2198528, which added an auto guard to checkStandbyStatus. This was later reverted in commit 103f686 as part of PR #169 to avoid a breaking change in a minor release.

We fully understand the concern around breaking changes — the periodic standby status has likely been relied upon by existing users, even in auto: false configurations. That said, the current behavior can lead to silent data loss in manual-ack workflows, which is arguably more surprising than a documented behavioral change.

Would it make sense to reconsider the original fix and ship it as part of a major release? That way existing users get fair notice via the version bump, and users relying on auto: false for strict manual acknowledgment get the semantics they'd expect.

An even better middle ground might be to track _lastAcknowledgedLsn separately, so the timer can still send keepalives (using the last safe position) without advancing past unprocessed messages. This would preserve connection health for all users while respecting the auto: false contract.

Workaround:

Set timeoutSeconds: 0 to disable the timer entirely. For short-lived consumers (e.g., Lambda), the connection won't be dropped within wal_sender_timeout. For long-lived consumers, implement a manual heartbeat using the heartbeat event and acknowledging with the last successfully processed LSN.

Affected version: 2.4.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions