diff --git a/src/backend/executor/nodeCtescan.c b/src/backend/executor/nodeCtescan.c
index 9ea318ae7da..ae7bfce4e5e 100644
--- a/src/backend/executor/nodeCtescan.c
+++ b/src/backend/executor/nodeCtescan.c
@@ -107,6 +107,13 @@ CteScanNext(CteScanState *node)
 			return NULL;
 		}
 
+		/*
+		 * There are corner cases where the subplan could change which
+		 * tuplestore read pointer is active, so be sure to reselect ours
+		 * before storing the tuple we got.
+		 */
+		tuplestore_select_read_pointer(tuplestorestate, node->readptr);
+
 		/*
 		 * Append a copy of the returned tuple to tuplestore.  NOTE: because
 		 * our read pointer is certainly in EOF state, its read position will
@@ -176,6 +183,12 @@ ExecInitCteScan(CteScan *node, EState *estate, int eflags)
 	 * we might be asked to rescan the CTE even though upper levels didn't
 	 * tell us to be prepared to do it efficiently.  Annoying, since this
 	 * prevents truncation of the tuplestore.  XXX FIXME
+	 *
+	 * Note: if we are in an EPQ recheck plan tree, it's likely that no access
+	 * to the tuplestore is needed at all, making this even more annoying.
+	 * It's not worth improving that as long as all the read pointers would
+	 * have REWIND anyway, but if we ever improve this logic then that aspect
+	 * should be considered too.
 	 */
 	eflags |= EXEC_FLAG_REWIND;
 
diff --git a/src/test/isolation/expected/eval-plan-qual.out b/src/test/isolation/expected/eval-plan-qual.out
index 99c4137e14a..be87d1b0c27 100644
--- a/src/test/isolation/expected/eval-plan-qual.out
+++ b/src/test/isolation/expected/eval-plan-qual.out
@@ -125,3 +125,18 @@ step readwcte: <... completed>
 id             value          
 
 1              tableAValue2   
+
+starting permutation: wrtwcte multireadwcte c1 c2
+step wrtwcte: UPDATE table_a SET value = 'tableAValue2' WHERE id = 1;
+step multireadwcte: 
+	WITH updated AS (
+	  UPDATE table_a SET value = 'tableAValue3' WHERE id = 1 RETURNING id
+	)
+	SELECT (SELECT id FROM updated) AS subid, * FROM updated;
+ <waiting ...>
+step c1: COMMIT;
+step c2: COMMIT;
+step multireadwcte: <... completed>
+subid          id             
+
+1              1              
diff --git a/src/test/isolation/specs/eval-plan-qual.spec b/src/test/isolation/specs/eval-plan-qual.spec
index f15dee385fc..749c6405796 100644
--- a/src/test/isolation/specs/eval-plan-qual.spec
+++ b/src/test/isolation/specs/eval-plan-qual.spec
@@ -94,6 +94,14 @@ step "readwcte"	{
 	SELECT * FROM cte2;
 }
 
+# this test exercises a different CTE misbehavior, cf bug #14870
+step "multireadwcte"	{
+	WITH updated AS (
+	  UPDATE table_a SET value = 'tableAValue3' WHERE id = 1 RETURNING id
+	)
+	SELECT (SELECT id FROM updated) AS subid, * FROM updated;
+}
+
 teardown	{ COMMIT; }
 
 permutation "wx1" "wx2" "c1" "c2" "read"
@@ -102,3 +110,4 @@ permutation "upsert1" "upsert2" "c1" "c2" "read"
 permutation "readp1" "writep1" "readp2" "c1" "c2"
 permutation "writep2" "returningp1" "c1" "c2"
 permutation "wrtwcte" "readwcte" "c1" "c2"
+permutation "wrtwcte" "multireadwcte" "c1" "c2"