diff --git a/contrib/test_decoding/expected/stream.out b/contrib/test_decoding/expected/stream.out
index a76f77601e..a4b51c0051 100644
--- a/contrib/test_decoding/expected/stream.out
+++ b/contrib/test_decoding/expected/stream.out
@@ -128,6 +128,29 @@ SELECT count(*) FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL,
      5
 (1 row)
 
+-- bug #19839
+--
+-- A subtransaction without decoded changes must not be marked as serialized
+-- when its parent transaction spills to disk during a partial TOAST change.
+-- Otherwise, truncating the transaction after streaming fails on the
+-- subtransaction's invalid final_lsn.
+BEGIN;
+SAVEPOINT s;
+SELECT 1 FROM stream_test LIMIT 1 FOR UPDATE;
+ ?column? 
+----------
+        1
+(1 row)
+
+RELEASE SAVEPOINT s;
+INSERT INTO stream_test VALUES (repeat('a', 100000));
+COMMIT;
+SELECT count(*) FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1', 'stream-changes', '1');
+ count 
+-------
+     4
+(1 row)
+
 DROP TABLE stream_test;
 SELECT pg_drop_replication_slot('regression_slot');
  pg_drop_replication_slot 
diff --git a/contrib/test_decoding/sql/stream.sql b/contrib/test_decoding/sql/stream.sql
index 7f43f0c2ab..22a4e1255b 100644
--- a/contrib/test_decoding/sql/stream.sql
+++ b/contrib/test_decoding/sql/stream.sql
@@ -59,5 +59,19 @@ ROLLBACK TO s1;
 COMMIT;
 SELECT count(*) FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1', 'stream-changes', '1');
 
+-- bug #19839
+--
+-- A subtransaction without decoded changes must not be marked as serialized
+-- when its parent transaction spills to disk during a partial TOAST change.
+-- Otherwise, truncating the transaction after streaming fails on the
+-- subtransaction's invalid final_lsn.
+BEGIN;
+SAVEPOINT s;
+SELECT 1 FROM stream_test LIMIT 1 FOR UPDATE;
+RELEASE SAVEPOINT s;
+INSERT INTO stream_test VALUES (repeat('a', 100000));
+COMMIT;
+SELECT count(*) FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1', 'stream-changes', '1');
+
 DROP TABLE stream_test;
 SELECT pg_drop_replication_slot('regression_slot');
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 50989db6c4..1eb42b1396 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -4049,12 +4049,21 @@ ReorderBufferSerializeTXN(ReorderBuffer *rb, ReorderBufferTXN *txn)
 
 		/* update the decoding stats */
 		UpdateDecodingStats((LogicalDecodingContext *) rb->private_data);
+
+		/*
+		 * Mark the transaction as serialized only if something was written,
+		 * and only after the check above.  final_lsn is advanced for each
+		 * change written, so a transaction that wrote nothing, such as a
+		 * subtransaction without decoded changes, would be marked with an
+		 * invalid final_lsn, and ReorderBufferRestoreCleanup() would later
+		 * fail on it.
+		 */
+		txn->txn_flags |= RBTXN_IS_SERIALIZED;
 	}
 
 	Assert(spilled == txn->nentries_mem);
 	Assert(dlist_is_empty(&txn->changes));
 	txn->nentries_mem = 0;
-	txn->txn_flags |= RBTXN_IS_SERIALIZED;
 
 	if (fd != -1)
 		CloseTransientFile(fd);
