diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 00a8327e77..bf5cf416bf 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -467,6 +467,9 @@ ReorderBufferReturnTXN(ReorderBuffer *rb, ReorderBufferTXN *txn)
 	/* Reset the toast hash */
 	ReorderBufferToastReset(rb, txn);
 
+	/* All changes must be returned */
+	Assert(txn->size == 0);
+
 	pfree(txn);
 }
 
@@ -1506,6 +1509,7 @@ ReorderBufferCleanupTXN(ReorderBuffer *rb, ReorderBufferTXN *txn)
 {
 	bool		found;
 	dlist_mutable_iter iter;
+	Size		mem_freed = 0;
 
 	/* cleanup subtransactions & their changes */
 	dlist_foreach_modify(iter, &txn->subtxns)
@@ -1535,9 +1539,20 @@ ReorderBufferCleanupTXN(ReorderBuffer *rb, ReorderBufferTXN *txn)
 		/* Check we're not mixing changes from different transactions. */
 		Assert(change->txn == txn);
 
+		/*
+		 * Instead of updating the memory counter for individual changes,
+		 * we sum up the size of memory to free so we can update the memory
+		 * counter all together below. This saves costs of maintaining
+		 * the max-heap.
+		 */
+		mem_freed += ReorderBufferChangeSize(change);
+
 		ReorderBufferReturnChange(rb, change, false);
 	}
 
+	/* Update the memory counter */
+	ReorderBufferChangeMemoryUpdate(rb, NULL, txn, false, mem_freed);
+
 	/*
 	 * Cleanup the tuplecids we stored for decoding catalog snapshot access.
 	 * They are always stored in the toplevel transaction.
@@ -1594,9 +1609,6 @@ ReorderBufferCleanupTXN(ReorderBuffer *rb, ReorderBufferTXN *txn)
 	if (rbtxn_is_serialized(txn))
 		ReorderBufferRestoreCleanup(rb, txn);
 
-	/* Update the memory counter */
-	ReorderBufferChangeMemoryUpdate(rb, NULL, txn, false, txn->size);
-
 	/* deallocate */
 	ReorderBufferReturnTXN(rb, txn);
 }
@@ -1616,6 +1628,7 @@ static void
 ReorderBufferTruncateTXN(ReorderBuffer *rb, ReorderBufferTXN *txn, bool txn_prepared)
 {
 	dlist_mutable_iter iter;
+	Size	mem_freed = 0;
 
 	/* cleanup subtransactions & their changes */
 	dlist_foreach_modify(iter, &txn->subtxns)
@@ -1648,11 +1661,19 @@ ReorderBufferTruncateTXN(ReorderBuffer *rb, ReorderBufferTXN *txn, bool txn_prep
 		/* remove the change from it's containing list */
 		dlist_delete(&change->node);
 
+		/*
+		 * Instead of updating the memory counter for individual changes,
+		 * we sum up the size of memory to free so we can update the memory
+		 * counter all together below. This saves costs of maintaining
+		 * the max-heap.
+		 */
+		mem_freed += ReorderBufferChangeSize(change);
+
 		ReorderBufferReturnChange(rb, change, false);
 	}
 
 	/* Update the memory counter */
-	ReorderBufferChangeMemoryUpdate(rb, NULL, txn, false, txn->size);
+	ReorderBufferChangeMemoryUpdate(rb, NULL, txn, false, mem_freed);
 
 	/*
 	 * Mark the transaction as streamed.
@@ -2062,6 +2083,9 @@ ReorderBufferResetTXN(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		rb->stream_stop(rb, txn, last_lsn);
 		ReorderBufferSaveTXNSnapshot(rb, txn, snapshot_now, command_id);
 	}
+
+	/* All changes must be returned */
+	Assert(txn->size == 0);
 }
 
 /*
diff --git a/src/test/subscription/t/031_column_list.pl b/src/test/subscription/t/031_column_list.pl
index 9a97fa5020..4106c29350 100644
--- a/src/test/subscription/t/031_column_list.pl
+++ b/src/test/subscription/t/031_column_list.pl
@@ -1255,7 +1255,7 @@ is( $node_subscriber->safe_psql(
 
 $node_publisher->safe_psql(
 	'postgres', qq(
-	CREATE TABLE test_mix_1 (a int PRIMARY KEY, b int, c int);
+	CREATE TABLE test_mix_1 (a int PRIMARY KEY, b int, c text);
 	CREATE PUBLICATION pub_mix_1 FOR TABLE test_mix_1 (a, b);
 	CREATE PUBLICATION pub_mix_2 FOR TABLE test_mix_1 (a, c);
 ));
@@ -1263,7 +1263,7 @@ $node_publisher->safe_psql(
 $node_subscriber->safe_psql(
 	'postgres', qq(
 	DROP SUBSCRIPTION sub1;
-	CREATE TABLE test_mix_1 (a int PRIMARY KEY, b int, c int);
+	CREATE TABLE test_mix_1 (a int PRIMARY KEY, b int, c text);
 ));
 
 my ($cmdret, $stdout, $stderr) = $node_subscriber->psql(
@@ -1290,10 +1290,14 @@ $node_subscriber->safe_psql(
 
 $node_publisher->wait_for_catchup('sub1');
 
+# XXX: walsender raises an error during the replay of the INSERT transaction. Then during cleanup, it frees
+# the transaction entry while having the TOAST changes in memory. Previously, since we zeroed the transaction
+# size before freeing TOAST changes, we ended up further subtracting the memory counter from zero, resulting
+# in an assertion failure (with assertion build) or SEGV (wihtout assertion build).
 $node_publisher->safe_psql(
 	'postgres', qq(
 	ALTER PUBLICATION pub_mix_1 SET TABLE test_mix_1 (a, b);
-	INSERT INTO test_mix_1 VALUES(1, 1, 1);
+	INSERT INTO test_mix_1 (a, c) SELECT 1, repeat(string_agg(to_char(g.i, 'FM0000'), ''), 50) FROM generate_series(1, 500) g(i);
 ));
 
 $offset = $node_publisher->wait_for_log(
