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
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:
The private _acknowledge() correctly checks auto:
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):
###History:
We noticed that this exact issue was already identified and fixed in commit 2198528, which added an
autoguard tocheckStandbyStatus. 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: 0to disable the timer entirely. For short-lived consumers (e.g., Lambda), the connection won't be dropped withinwal_sender_timeout. For long-lived consumers, implement a manual heartbeat using theheartbeatevent and acknowledging with the last successfully processed LSN.Affected version: 2.4.0