diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c
index 2b5f8ef1e80..06ee8565b80 100644
--- a/src/backend/access/heap/pruneheap.c
+++ b/src/backend/access/heap/pruneheap.c
@@ -38,7 +38,7 @@ typedef struct
 	bool		mark_unused_now;
 
 	TransactionId new_prune_xid;	/* new prune hint value for page */
-	TransactionId snapshotConflictHorizon;	/* latest xid removed */
+	TransactionId latest_xid_removed;
 	int			nredirected;	/* numbers of entries in arrays below */
 	int			ndead;
 	int			nunused;
@@ -176,7 +176,7 @@ heap_page_prune_opt(Relation relation, Buffer buffer)
 			 * not the relation has indexes, since we cannot safely determine
 			 * that during on-access pruning with the current implementation.
 			 */
-			heap_page_prune_and_freeze(relation, buffer, vistest, false, NULL,
+			heap_page_prune_and_freeze(relation, buffer, false, vistest, false, NULL,
 									   &presult, NULL);
 
 			/*
@@ -214,6 +214,15 @@ heap_page_prune_opt(Relation relation, Buffer buffer)
  * Prune and repair fragmentation and potentially freeze tuples on the
  * specified page.
  *
+ * heap_page_prune_and_freeze() may do all of the following: prune tuples,
+ * freeze tuples, determine the all-visible/all-frozen status of the apge and
+ * the associated snapshot conflict horizon, determine if truncating this page
+ * would be safe, and identify any new potential values for relfrozenxid and
+ * relminmxid. Not all of these responsibilities are useful to all callers. If
+ * by_vacuum is passed as True, heap_page_prune_and_freeze() will do all of
+ * these. Otherwise, the other parameters will determine which of these it
+ * does.
+ *
  * If the page can be marked all-frozen in the visibility map, we may
  * opportunistically freeze tuples on the page if either its tuples are old
  * enough or freezing will be cheap enough.
@@ -242,6 +251,7 @@ heap_page_prune_opt(Relation relation, Buffer buffer)
  */
 void
 heap_page_prune_and_freeze(Relation relation, Buffer buffer,
+						   bool by_vacuum,
 						   GlobalVisState *vistest,
 						   bool mark_unused_now,
 						   HeapPageFreeze *pagefrz,
@@ -254,6 +264,7 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
 				maxoff;
 	PruneState	prstate;
 	HeapTupleData tup;
+	TransactionId visibility_cutoff_xid;
 	bool		do_freeze;
 	bool		do_prune;
 	bool		do_hint;
@@ -275,7 +286,7 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
 	prstate.new_prune_xid = InvalidTransactionId;
 	prstate.vistest = vistest;
 	prstate.mark_unused_now = mark_unused_now;
-	prstate.snapshotConflictHorizon = InvalidTransactionId;
+	prstate.latest_xid_removed = InvalidTransactionId;
 	prstate.nredirected = prstate.ndead = prstate.nunused = 0;
 	memset(prstate.marked, 0, sizeof(prstate.marked));
 
@@ -302,8 +313,16 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
 	 * all_visible is also set to true.
 	 */
 	presult->all_frozen = true;
-	/* for recovery conflicts */
-	presult->frz_conflict_horizon = InvalidTransactionId;
+
+	/*
+	 * The visibility cutoff xid is the newest xmin of live tuples on the
+	 * page. In the common case, this will be set as the conflict horizon the
+	 * caller can use for updating the VM. If, at the end of freezing and
+	 * pruning, the page is all-frozen, there is no possibility that any
+	 * running transaction on the standby does not see tuples on the page as
+	 * all-visible, so the conflict horizon remains InvalidTransactionId.
+	 */
+	presult->vm_conflict_horizon = visibility_cutoff_xid = InvalidTransactionId;
 
 	/* For advancing relfrozenxid and relminmxid */
 	presult->new_relfrozenxid = InvalidTransactionId;
@@ -362,6 +381,13 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
 		prstate.htsv[offnum] = heap_prune_satisfies_vacuum(&prstate, &tup,
 														   buffer);
 
+		/*
+		 * On-access pruning does not update the VM nor provide pagefrz to
+		 * consider freezing tuples, so skip the rest of the loop.
+		 */
+		if (!by_vacuum)
+			continue;
+
 		/*
 		 * The criteria for counting a tuple as live in this block need to
 		 * match what analyze.c's acquire_sample_rows() does, otherwise VACUUM
@@ -434,17 +460,19 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
 					 * don't consider the page all-visible.
 					 */
 					xmin = HeapTupleHeaderGetXmin(htup);
-					if (xmin != FrozenTransactionId &&
-						!GlobalVisTestIsRemovableXid(vistest, xmin))
+
+					/* For now always use pagefrz->cutoffs */
+					Assert(pagefrz);
+					if (!TransactionIdPrecedes(xmin, pagefrz->cutoffs->OldestXmin))
 					{
 						all_visible_except_removable = false;
 						break;
 					}
 
 					/* Track newest xmin on page. */
-					if (TransactionIdFollows(xmin, presult->frz_conflict_horizon) &&
+					if (TransactionIdFollows(xmin, visibility_cutoff_xid) &&
 						TransactionIdIsNormal(xmin))
-						presult->frz_conflict_horizon = xmin;
+						visibility_cutoff_xid = xmin;
 				}
 				break;
 			case HEAPTUPLE_RECENTLY_DEAD:
@@ -673,10 +701,9 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
 
 	if (do_prune || do_freeze)
 	{
-		/*
-		 * Apply the planned item changes, then repair page fragmentation, and
-		 * update the page's hint bit about whether it has free line pointers.
-		 */
+		TransactionId frz_conflict_horizon = InvalidTransactionId;
+
+		/* Apply the planned item changes and repair page fragmentation. */
 		if (do_prune)
 		{
 			heap_page_prune_execute(buffer, false,
@@ -692,12 +719,16 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
 			 * when the whole page is eligible to become all-frozen in the VM
 			 * once we're done with it.  Otherwise we generate a conservative
 			 * cutoff by stepping back from OldestXmin. This avoids false
-			 * conflicts when hot_standby_feedback is in use.
+			 * conflicts when hot_standby_feedback is in use. MFIXME: it is
+			 * possible to use presult->all_visible by now, but is it clearer
+			 * to use all_visible_except_removable?
 			 */
-			if (!(all_visible_except_removable && presult->all_frozen))
+			if (all_visible_except_removable && presult->all_frozen)
+				frz_conflict_horizon = visibility_cutoff_xid;
+			else
 			{
-				presult->frz_conflict_horizon = pagefrz->cutoffs->OldestXmin;
-				TransactionIdRetreat(presult->frz_conflict_horizon);
+				frz_conflict_horizon = pagefrz->cutoffs->OldestXmin;
+				TransactionIdRetreat(frz_conflict_horizon);
 			}
 			heap_freeze_prepared_tuples(buffer, prstate.frozen, presult->nfrozen);
 		}
@@ -709,22 +740,22 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
 		 */
 		if (RelationNeedsWAL(relation))
 		{
+			TransactionId conflict_xid = InvalidTransactionId;
+
 			/*
-			 * The snapshotConflictHorizon for the whole record should be the most
-			 * conservative of all the horizons calculated for any of the possible
-			 * modifications. If this record will prune tuples, any transactions on
-			 * the standby older than the youngest xmax of the most recently removed
-			 * tuple this record will prune will conflict. If this record will freeze
-			 * tuples, any transactions on the standby with xids older than the
-			 * youngest tuple this record will freeze will conflict.
+			 * The snapshot conflict horizon for the whole record should be
+			 * the newest xid of all the horizons calculated for any of the
+			 * possible modifications. If this record will prune tuples, any
+			 * transactions on the standby older than the youngest xmax of the
+			 * most recently removed tuple this record will prune will
+			 * conflict. If this record will freeze tuples, any transactions
+			 * on the standby with xids older than the youngest tuple this
+			 * record will freeze will conflict.
 			 */
-			TransactionId conflict_xid;
-
-			if (do_freeze)
-				conflict_xid = Max(prstate.snapshotConflictHorizon,
-								   presult->frz_conflict_horizon);
+			if (TransactionIdFollows(frz_conflict_horizon, prstate.latest_xid_removed))
+				conflict_xid = frz_conflict_horizon;
 			else
-				conflict_xid = prstate.snapshotConflictHorizon;
+				conflict_xid = prstate.latest_xid_removed;
 
 			log_heap_prune_and_freeze(relation, buffer,
 									  conflict_xid,
@@ -738,6 +769,15 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer,
 
 	END_CRIT_SECTION();
 
+	/*
+	 * For callers planning to update the visibility map, the conflict horizon
+	 * for that record must be the newest xmin on the page. However, if the
+	 * page is completely frozen, there can be no conflict and the
+	 * vm_conflict_horizon should remain InvalidTransactionId.
+	 */
+	if (!presult->all_frozen)
+		presult->vm_conflict_horizon = visibility_cutoff_xid;
+
 	/*
 	 * If we froze tuples on the page, the caller can advance relfrozenxid and
 	 * relminmxid to the values in pagefrz->FreezePageRelfrozenXid and
@@ -876,7 +916,7 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum,
 			{
 				heap_prune_record_unused(prstate, rootoffnum);
 				HeapTupleHeaderAdvanceConflictHorizon(htup,
-													  &prstate->snapshotConflictHorizon);
+													  &prstate->latest_xid_removed);
 				ndeleted++;
 			}
 
@@ -1029,7 +1069,7 @@ heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum,
 		{
 			latestdead = offnum;
 			HeapTupleHeaderAdvanceConflictHorizon(htup,
-												  &prstate->snapshotConflictHorizon);
+												  &prstate->latest_xid_removed);
 		}
 		else if (!recent_dead)
 			break;
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index c3da64102cf..ee4fdc00073 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -1421,7 +1421,7 @@ lazy_scan_prune(LVRelState *vacrel,
 	 * items LP_UNUSED, so mark_unused_now should be true if no indexes and
 	 * false otherwise.
 	 */
-	heap_page_prune_and_freeze(rel, buf, vacrel->vistest, vacrel->nindexes == 0,
+	heap_page_prune_and_freeze(rel, buf, true, vacrel->vistest, vacrel->nindexes == 0,
 							   &pagefrz, &presult, &vacrel->offnum);
 
 	/*
@@ -1444,10 +1444,6 @@ lazy_scan_prune(LVRelState *vacrel,
 		 * (don't confuse that with pages newly set all-frozen in VM).
 		 */
 		vacrel->frozen_pages++;
-
-		/* Using same cutoff when setting VM is now unnecessary */
-		if (presult.all_frozen)
-			presult.frz_conflict_horizon = InvalidTransactionId;
 	}
 
 	/*
@@ -1473,7 +1469,7 @@ lazy_scan_prune(LVRelState *vacrel,
 		Assert(presult.all_frozen == debug_all_frozen);
 
 		Assert(!TransactionIdIsValid(debug_cutoff) ||
-			   debug_cutoff == presult.frz_conflict_horizon);
+			   debug_cutoff == presult.vm_conflict_horizon);
 	}
 #endif
 
@@ -1527,7 +1523,7 @@ lazy_scan_prune(LVRelState *vacrel,
 
 		if (presult.all_frozen)
 		{
-			Assert(!TransactionIdIsValid(presult.frz_conflict_horizon));
+			Assert(!TransactionIdIsValid(presult.vm_conflict_horizon));
 			flags |= VISIBILITYMAP_ALL_FROZEN;
 		}
 
@@ -1547,7 +1543,7 @@ lazy_scan_prune(LVRelState *vacrel,
 		PageSetAllVisible(page);
 		MarkBufferDirty(buf);
 		visibilitymap_set(vacrel->rel, blkno, buf, InvalidXLogRecPtr,
-						  vmbuffer, presult.frz_conflict_horizon,
+						  vmbuffer, presult.vm_conflict_horizon,
 						  flags);
 	}
 
@@ -1612,11 +1608,11 @@ lazy_scan_prune(LVRelState *vacrel,
 		/*
 		 * Set the page all-frozen (and all-visible) in the VM.
 		 *
-		 * We can pass InvalidTransactionId as our frz_conflict_horizon, since
-		 * a snapshotConflictHorizon sufficient to make everything safe for
-		 * REDO was logged when the page's tuples were frozen.
+		 * We can pass InvalidTransactionId as our snapshot conflict horizon,
+		 * since a snapshot conflict horizon sufficient to make everything
+		 * safe for REDO was logged when the page's tuples were frozen.
 		 */
-		Assert(!TransactionIdIsValid(presult.frz_conflict_horizon));
+		Assert(!TransactionIdIsValid(presult.vm_conflict_horizon));
 		visibilitymap_set(vacrel->rel, blkno, buf, InvalidXLogRecPtr,
 						  vmbuffer, InvalidTransactionId,
 						  VISIBILITYMAP_ALL_VISIBLE |
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index b2015f5a1ac..ba2ce01af04 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -212,8 +212,8 @@ typedef struct PruneFreezeResult
 	/* Whether or not the page can be set all frozen in the VM */
 	bool		all_frozen;
 
-	/* Number of newly frozen tuples */
-	TransactionId frz_conflict_horizon; /* Newest xmin on the page */
+	/* Newest xmin on the page */
+	TransactionId vm_conflict_horizon;
 
 	/* New value of relfrozenxid found by heap_page_prune_and_freeze() */
 	TransactionId new_relfrozenxid;
@@ -322,6 +322,7 @@ extern TransactionId heap_index_delete_tuples(Relation rel,
 struct GlobalVisState;
 extern void heap_page_prune_opt(Relation relation, Buffer buffer);
 extern void heap_page_prune_and_freeze(Relation relation, Buffer buffer,
+									   bool by_vacuum,
 									   struct GlobalVisState *vistest,
 									   bool mark_unused_now,
 									   HeapPageFreeze *pagefrz,
