diff --git a/src/backend/access/heap/heapam_indexscan.c b/src/backend/access/heap/heapam_indexscan.c
index 33d14f1de7d..590a26e6785 100644
--- a/src/backend/access/heap/heapam_indexscan.c
+++ b/src/backend/access/heap/heapam_indexscan.c
@@ -16,6 +16,8 @@
 
 #include "access/heapam.h"
 #include "access/relscan.h"
+#include "access/tableam.h"
+#include "executor/tuptable.h"
 #include "storage/predicate.h"
 
 
@@ -287,6 +289,34 @@ heapam_index_fetch_tuple(struct IndexFetchTableData *scan,
 
 		slot->tts_tableOid = RelationGetRelid(scan->rel);
 		ExecStoreBufferHeapTuple(&bslot->base.tupdata, slot, hscan->xs_cbuf);
+
+		/*
+		 * If requested, release the buffer pin early to avoid holding pins
+		 * across executor node boundaries, which can cause recovery conflicts
+		 * on standbys.
+		 *
+		 * SO_MATERIALIZE_ON_FETCH: materialize the tuple into the slot so
+		 * the caller can still read it, then release both pins.
+		 *
+		 * SO_RELEASE_PIN_ON_FETCH: just release the AM's private pin.
+		 * The slot retains its own pin (from ExecStoreBufferHeapTuple)
+		 * until the caller clears it. Use this when the caller doesn't
+		 * need the heap tuple data (e.g. index-only scans that only use
+		 * the visibility answer).
+		 */
+		if (hscan->xs_base.flags & SO_MATERIALIZE_ON_FETCH)
+		{
+			ExecMaterializeSlot(slot);
+			ReleaseBuffer(hscan->xs_cbuf);
+			hscan->xs_cbuf = InvalidBuffer;
+			hscan->xs_blk = InvalidBlockNumber;
+		}
+		else if (hscan->xs_base.flags & SO_RELEASE_PIN_ON_FETCH)
+		{
+			ReleaseBuffer(hscan->xs_cbuf);
+			hscan->xs_cbuf = InvalidBuffer;
+			hscan->xs_blk = InvalidBlockNumber;
+		}
 	}
 	else
 	{
diff --git a/src/backend/executor/nodeIndexonlyscan.c b/src/backend/executor/nodeIndexonlyscan.c
index d52012e8a69..bc0ff987671 100644
--- a/src/backend/executor/nodeIndexonlyscan.c
+++ b/src/backend/executor/nodeIndexonlyscan.c
@@ -85,19 +85,35 @@ IndexOnlyNext(IndexOnlyScanState *node)
 
 	if (scandesc == NULL)
 	{
+		uint32		scan_flags;
+
 		/*
 		 * We reach here if the index only scan is not parallel, or if we're
 		 * serially executing an index only scan that was planned to be
 		 * parallel.
 		 */
+		scan_flags = ScanRelIsReadOnly(&node->ss) ?
+			SO_HINT_REL_READ_ONLY : SO_NONE;
+
+		/*
+		 * For unique index scans with equality on all key columns, release
+		 * the heap buffer pin immediately after the visibility check. IOS
+		 * fills its output slot from index data, so we don't need the heap
+		 * tuple — just the visibility answer. The slot's own pin (from
+		 * ExecStoreBufferHeapTuple) is released by the ExecClearTuple call
+		 * that follows index_fetch_heap.
+		 */
+		if (node->ioss_RelationDesc->rd_index->indisunique &&
+			node->ioss_NumScanKeys >= node->ioss_RelationDesc->rd_index->indnkeyatts)
+			scan_flags |= SO_RELEASE_PIN_ON_FETCH;
+
 		scandesc = index_beginscan(node->ss.ss_currentRelation,
 								   node->ioss_RelationDesc,
 								   estate->es_snapshot,
 								   node->ioss_Instrument,
 								   node->ioss_NumScanKeys,
 								   node->ioss_NumOrderByKeys,
-								   ScanRelIsReadOnly(&node->ss) ?
-								   SO_HINT_REL_READ_ONLY : SO_NONE);
+								   scan_flags);
 
 		node->ioss_ScanDesc = scandesc;
 
diff --git a/src/backend/executor/nodeIndexscan.c b/src/backend/executor/nodeIndexscan.c
index 39f6691ee35..05e3316f577 100644
--- a/src/backend/executor/nodeIndexscan.c
+++ b/src/backend/executor/nodeIndexscan.c
@@ -104,18 +104,32 @@ IndexNext(IndexScanState *node)
 
 	if (scandesc == NULL)
 	{
+		uint32		scan_flags;
+
 		/*
 		 * We reach here if the index scan is not parallel, or if we're
 		 * serially executing an index scan that was planned to be parallel.
 		 */
+		scan_flags = ScanRelIsReadOnly(&node->ss) ?
+			SO_HINT_REL_READ_ONLY : SO_NONE;
+
+		/*
+		 * For unique index scans with equality on all key columns,
+		 * materialize the tuple on fetch to release the buffer pin
+		 * immediately.  This avoids holding pins across executor node
+		 * boundaries, reducing recovery conflicts on standbys.
+		 */
+		if (node->iss_RelationDesc->rd_index->indisunique &&
+			node->iss_NumScanKeys >= node->iss_RelationDesc->rd_index->indnkeyatts)
+			scan_flags |= SO_MATERIALIZE_ON_FETCH;
+
 		scandesc = index_beginscan(node->ss.ss_currentRelation,
 								   node->iss_RelationDesc,
 								   estate->es_snapshot,
 								   node->iss_Instrument,
 								   node->iss_NumScanKeys,
 								   node->iss_NumOrderByKeys,
-								   ScanRelIsReadOnly(&node->ss) ?
-								   SO_HINT_REL_READ_ONLY : SO_NONE);
+								   scan_flags);
 
 		node->iss_ScanDesc = scandesc;
 
@@ -202,18 +216,26 @@ IndexNextWithReorder(IndexScanState *node)
 
 	if (scandesc == NULL)
 	{
+		uint32		scan_flags;
+
 		/*
 		 * We reach here if the index scan is not parallel, or if we're
 		 * serially executing an index scan that was planned to be parallel.
 		 */
+		scan_flags = ScanRelIsReadOnly(&node->ss) ?
+			SO_HINT_REL_READ_ONLY : SO_NONE;
+
+		if (node->iss_RelationDesc->rd_index->indisunique &&
+			node->iss_NumScanKeys >= node->iss_RelationDesc->rd_index->indnkeyatts)
+			scan_flags |= SO_MATERIALIZE_ON_FETCH;
+
 		scandesc = index_beginscan(node->ss.ss_currentRelation,
 								   node->iss_RelationDesc,
 								   estate->es_snapshot,
 								   node->iss_Instrument,
 								   node->iss_NumScanKeys,
 								   node->iss_NumOrderByKeys,
-								   ScanRelIsReadOnly(&node->ss) ?
-								   SO_HINT_REL_READ_ONLY : SO_NONE);
+								   scan_flags);
 
 		node->iss_ScanDesc = scandesc;
 
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index f2c36696bca..55b41cd676f 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -72,6 +72,12 @@ typedef enum ScanOptions
 
 	/* collect scan instrumentation */
 	SO_SCAN_INSTRUMENT = 1 << 11,
+
+	/* materialize tuple on fetch, releasing the buffer pin immediately */
+	SO_MATERIALIZE_ON_FETCH = 1 << 12,
+
+	/* release the AM's buffer pin on fetch; slot retains its own pin until cleared */
+	SO_RELEASE_PIN_ON_FETCH = 1 << 13,
 }			ScanOptions;
 
 /*
