Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1xBphZ-00000003PR6-1hir for pgsql-bugs@arkaria.postgresql.org; Wed, 30 Sep 2026 08:30:09 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.98.2) (envelope-from ) id 1xBphY-000000012xb-1Rpf for pgsql-bugs@arkaria.postgresql.org; Wed, 30 Sep 2026 08:30:08 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1xBlyH-000000009DD-01HD for pgsql-bugs@lists.postgresql.org; Wed, 30 Sep 2026 04:31:09 +0000 Received: from mahout.postgresql.org ([2001:4800:3e1:1::227]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1xBlyE-00000001ygu-0fc7 for pgsql-bugs@lists.postgresql.org; Wed, 30 Sep 2026 04:31:08 +0000 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=postgresql.org; s=20171124; h=Message-ID:Date:Reply-To:Cc:From:To:Subject: Content-Transfer-Encoding:MIME-Version:Content-Type:Sender:Content-ID: Content-Description:In-Reply-To:References; bh=oMgs5I2yVpilLeCP163DnuRMxTcf27c9grrbvBAnND0=; b=kWwNeNYRKUEXU4mZYEIjNNDvFP Bjv1jfzFSLOIgFqNTgeu48vBv7+Mpwutc94RKLrbjb+/ok6wWOladDLfY/qB/I07TtqX2nhAdVYcz s6WOucY902Fw+dEykNni8a2DouLm0UI5zTW+RtSGOpePpgszynZ1npbBOn8pIopucPcRwksjNHzeH f9sm5fIe6CzohzcvmvCiqMOdaO60bWqBnxOazAmSv4Tveco1wAdCAQFd3LukOBwJsp6mQ3sgWvS6D 89N4EotnBq9VD/irtDBoQ/jA7nMsnpVqFck6SUOQpb3P9IfWgsPb6Wr57+fugYm5CThK3OdOE+ZYG Sp5MKHlw==; Received: from wrigleys.postgresql.org ([2a02:16a8:dc51::60]) by mahout.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1xBlyD-000l2U-2E for pgsql-bugs@lists.postgresql.org; Wed, 30 Sep 2026 04:31:05 +0000 Received: from localhost ([127.0.0.1] helo=wrigleys.postgresql.org) by wrigleys.postgresql.org with esmtp (Exim 4.98.2) (envelope-from ) id 1xBlyB-0000000H1Np-3hIR for pgsql-bugs@lists.postgresql.org; Wed, 30 Sep 2026 04:31:03 +0000 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Subject: BUG #19733: Row not visible to a new snapshot after its transactional logical decoding message has been streamed To: pgsql-bugs@lists.postgresql.org From: PG Bug reporting form Cc: yk.verma2000@gmail.com Reply-To: yk.verma2000@gmail.com, pgsql-bugs@lists.postgresql.org Date: Wed, 30 Sep 2026 04:30:43 +0000 Message-ID: <19733-5fcd39e85fa221a4@postgresql.org> X-Auto-Response-Suppress: All Auto-Submitted: auto-generated List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk The following bug has been logged on the website: Bug reference: 19733 Logged by: Yash Kumar Verma Email address: yk.verma2000@gmail.com PostgreSQL version: 18.6 Operating system: Linux 6.10.14 aarch64, Debian 13 (official postg Description: =20 A transaction updates a row, calls pg_logical_emit_message(true, ...) with the row's new version, and commits. A client streaming the slot with pg_recvlogical receives the message and then runs a SELECT on that row from a separate ordinary session (READ COMMITTED, autocommit, same server). Occasionally that SELECT returns the row version from before the update. So a transaction's changes can be invisible to a new snapshot taken after the client has received that transaction's decoded output. We hit this in production, where a service uses transactional logical messages as an outbox. The consumer re-reads the row when it gets the message and sometimes reads stale data. Our current workaround is to sleep 100 ms before the read. Full scripts and results: https://github.com/YashKumarVerma/postgres-non-linear-reads-report EXPECTED Once pg_recvlogical has received the decoded message from transaction X, a new snapshot taken after that point sees X as committed. The SELECT should never return a version lower than the one in the message. ACTUAL One 30-second run per version, 32 pgbench clients rate-limited to 7000 tps: 16.15 210309 messages 28 stale reads 17.11 210321 messages 14 stale reads 18.6 210108 messages 21 stale reads 19beta4 209800 messages 13 stale reads Sample output (every stale read is exactly one version behind): STALE id=3D9 message_version=3D497 select_returned=3D496 STALE id=3D10 message_version=3D974 select_returned=3D973 STALE id=3D9 message_version=3D1072 select_returned=3D1071 A separate Go client (pgx + pglogrepl, pgoutput with messages 'true', 8 writers, 16000 messages per run, 3 runs per version) sees 12-38 stale reads per run on 16, 17, 18 and 19beta4. The row becomes visible 0.1-5.4 ms after the message arrives. With that client, a single writer never reproduced it, and reading with SELECT ... FOR SHARE instead of a plain SELECT never reproduced it. CONFIGURATION Official postgres Docker image, default postgresql.conf, plus only -c wal_level=3Dlogical. synchronous_commit =3D on, synchronous_standby_names =3D '' (defaults). No standbys, no other replication clients. STEPS TO REPRODUCE docker run -d --name walrace -e POSTGRES_HOST_AUTH_METHOD=3Dtrust \ postgres:18 -c wal_level=3Dlogical docker cp sql walrace:/sql # sql/ directory from the repo above docker exec walrace /sql/repro.sh --- setup.sql DROP TABLE IF EXISTS txn; CREATE TABLE txn (id bigint PRIMARY KEY, version bigint NOT NULL); INSERT INTO txn SELECT g, 0 FROM generate_series(1, 64) g; SELECT pg_drop_replication_slot('wal_race') FROM pg_replication_slots WHERE slot_name =3D 'wal_race'; SELECT pg_create_logical_replication_slot('wal_race', 'test_decoding'); --- writer.sql (pgbench script; each client owns row client_id + 1) BEGIN; UPDATE txn SET version =3D version + 1 WHERE id =3D :client_id + 1 RETURNING version \gset SELECT pg_logical_emit_message(true, 'wal_race', (:client_id + 1) || ':' || :version); COMMIT; --- consumer.sh (each streamed message becomes a SELECT in one long-lived --- psql session; it prints a row only when the version read is older) pg_recvlogical -U postgres -d postgres -S wal_race --start -f - -F 0 \ | sed -un 's/^message: transactional: 1 prefix: wal_race, sz: [0-9]* content:\([0-9]*\):\([0-9]*\)$/SELECT '"'"'STALE id=3D\1 message_version=3D= \2 select_returned=3D'"'"' || version FROM txn WHERE id =3D \1 AND version < \= 2;/p' \ | psql -U postgres -d postgres -At --- repro.sh psql -U postgres -d postgres -q -f setup.sql >/dev/null timeout 36 ./consumer.sh > stale.log 2>&1 & sleep 1 pgbench -U postgres -d postgres -n -c 32 -j 8 -R 7000 -T 30 -f writer.sql | grep "actually processed" wait || true cat stale.log echo "stale_reads=3D$(grep -c STALE stale.log || true)" pgbench is rate-limited because the single psql session in consumer.sh cannot keep up with unthrottled pgbench. If psql falls behind, it reads each row long after the message arrives, and nothing reproduces. PLATFORM Host: Apple M4 Pro, 24 GB RAM, macOS 27.0 Docker Desktop 28.3.2; VM: Linux 6.10.14-linuxkit aarch64, 12 CPUs, 8 GB RAM glibc 2.41 (Debian 13) Images: postgres:16, :17, :18, :19beta4, pulled 2026-09-29