* so we need no external state checks to decide what to do.  (This is good
  * because this function is applied during WAL recovery, when we don't have
  * access to any such state, and can't depend on the hint bits to be set.)
+ * There is an exception we make which is to assume GetMultiXactIdMembers can
+ * be called during recovery.
  *
  * Similarly, cutoff_multi must be less than or equal to the smallest
  * MultiXactId used by any transaction currently open.
  * exclusive lock ensures no other backend is in process of checking the
  * tuple status.  Also, getting exclusive lock makes it safe to adjust the
  * infomask bits.
+ *
+ * NB: Cannot rely on hint bits here, they might not be set after a crash or
+ * on a standby.
  */
 bool
 heap_freeze_tuple(HeapTupleHeader tuple, TransactionId cutoff_xid,
                  MultiXactId cutoff_multi)
 {
    bool        changed = false;
+   bool        freeze_xmax = false;
    TransactionId xid;
 
+   /* Process xmin */
    xid = HeapTupleHeaderGetXmin(tuple);
    if (TransactionIdIsNormal(xid) &&
        TransactionIdPrecedes(xid, cutoff_xid))
    }
 
    /*
-    * Note that this code handles IS_MULTI Xmax values, too, but only to mark
-    * the tuple as not updated if the multixact is below the cutoff Multixact
-    * given; it doesn't remove dead members of a very old multixact.
+    * Process xmax.  To thoroughly examine the current Xmax value we need to
+    * resolve a MultiXactId to its member Xids, in case some of them are
+    * below the given cutoff for Xids.  In that case, those values might need
+    * freezing, too.  Also, if a multi needs freezing, we cannot simply take
+    * it out --- if there's a live updater Xid, it needs to be kept.
+    *
+    * Make sure to keep heap_tuple_needs_freeze in sync with this.
     */
    xid = HeapTupleHeaderGetRawXmax(tuple);
-   if ((tuple->t_infomask & HEAP_XMAX_IS_MULTI) ?
-       (MultiXactIdIsValid(xid) &&
-        MultiXactIdPrecedes(xid, cutoff_multi)) :
-       (TransactionIdIsNormal(xid) &&
-        TransactionIdPrecedes(xid, cutoff_xid)))
+
+   if (tuple->t_infomask & HEAP_XMAX_IS_MULTI)
+   {
+       if (!MultiXactIdIsValid(xid))
+       {
+           /* no xmax set, ignore */
+           ;
+       }
+       else if (MultiXactIdPrecedes(xid, cutoff_multi))
+       {
+           /*
+            * This old multi cannot possibly be running.  If it was a locker
+            * only, it can be removed without much further thought; but if it
+            * contained an update, we need to preserve it.
+            */
+           if (HEAP_XMAX_IS_LOCKED_ONLY(tuple->t_infomask))
+               freeze_xmax = true;
+           else
+           {
+               TransactionId update_xid;
+
+               update_xid = HeapTupleGetUpdateXid(tuple);
+
+               /*
+                * The multixact has an update hidden within.  Get rid of it.
+                *
+                * If the update_xid is below the cutoff_xid, it necessarily
+                * must be an aborted transaction.  In a primary server, such
+                * an Xmax would have gotten marked invalid by
+                * HeapTupleSatisfiesVacuum, but in a replica that is not
+                * called before we are, so deal with it in the same way.
+                *
+                * If not below the cutoff_xid, then the tuple would have been
+                * pruned by vacuum, if the update committed long enough ago,
+                * and we wouldn't be freezing it; so it's either recently
+                * committed, or in-progress.  Deal with this by setting the
+                * Xmax to the update Xid directly and remove the IS_MULTI
+                * bit.  (We know there cannot be running lockers in this
+                * multi, because it's below the cutoff_multi value.)
+                */
+
+               if (TransactionIdPrecedes(update_xid, cutoff_xid))
+               {
+                   Assert(InRecovery || TransactionIdDidAbort(update_xid));
+                   freeze_xmax = true;
+               }
+               else
+               {
+                   Assert(InRecovery || !TransactionIdIsInProgress(update_xid));
+                   tuple->t_infomask &= ~HEAP_XMAX_BITS;
+                   HeapTupleHeaderSetXmax(tuple, update_xid);
+                   changed = true;
+               }
+           }
+       }
+       else if (HEAP_XMAX_IS_LOCKED_ONLY(tuple->t_infomask))
+       {
+           /* newer than the cutoff, so don't touch it */
+           ;
+       }
+       else
+       {
+           TransactionId   update_xid;
+
+           /*
+            * This is a multixact which is not marked LOCK_ONLY, but which
+            * is newer than the cutoff_multi.  If the update_xid is below the
+            * cutoff_xid point, then we can just freeze the Xmax in the
+            * tuple, removing it altogether.  This seems simple, but there
+            * are several underlying assumptions:
+            *
+            * 1. A tuple marked by an multixact containing a very old
+            * committed update Xid would have been pruned away by vacuum; we
+            * wouldn't be freezing this tuple at all.
+            *
+            * 2. There cannot possibly be any live locking members remaining
+            * in the multixact.  This is because if they were alive, the
+            * update's Xid would had been considered, via the lockers'
+            * snapshot's Xmin, as part the cutoff_xid.
+            *
+            * 3. We don't create new MultiXacts via MultiXactIdExpand() that
+            * include a very old aborted update Xid: in that function we only
+            * include update Xids corresponding to transactions that are
+            * committed or in-progress.
+            */
+           update_xid = HeapTupleGetUpdateXid(tuple);
+           if (TransactionIdPrecedes(update_xid, cutoff_xid))
+               freeze_xmax = true;
+       }
+   }
+   else if (TransactionIdIsNormal(xid) &&
+            TransactionIdPrecedes(xid, cutoff_xid))
+   {
+       freeze_xmax = true;
+   }
+
+   if (freeze_xmax)
    {
        HeapTupleHeaderSetXmax(tuple, InvalidTransactionId);
 
  *
  * It doesn't matter whether the tuple is alive or dead, we are checking
  * to see if a tuple needs to be removed or frozen to avoid wraparound.
+ *
+ * NB: Cannot rely on hint bits here, they might not be set after a crash or
+ * on a standby.
  */
 bool
 heap_tuple_needs_freeze(HeapTupleHeader tuple, TransactionId cutoff_xid,
        TransactionIdPrecedes(xid, cutoff_xid))
        return true;
 
-   if (!(tuple->t_infomask & HEAP_XMAX_INVALID))
+   /*
+    * The considerations for multixacts are complicated; look at
+    * heap_freeze_tuple for justifications.  This routine had better be in
+    * sync with that one!
+    */
+   if (tuple->t_infomask & HEAP_XMAX_IS_MULTI)
    {
-       if (!(tuple->t_infomask & HEAP_XMAX_IS_MULTI))
+       MultiXactId multi;
+
+       multi = HeapTupleHeaderGetRawXmax(tuple);
+       if (!MultiXactIdIsValid(multi))
        {
-           xid = HeapTupleHeaderGetRawXmax(tuple);
-           if (TransactionIdIsNormal(xid) &&
-               TransactionIdPrecedes(xid, cutoff_xid))
-               return true;
+           /* no xmax set, ignore */
+           ;
+       }
+       else if (MultiXactIdPrecedes(multi, cutoff_multi))
+           return true;
+       else if (HEAP_XMAX_IS_LOCKED_ONLY(tuple->t_infomask))
+       {
+           /* only-locker multis don't need internal examination */
+           ;
        }
        else
        {
-           MultiXactId multi;
-
-           multi = HeapTupleHeaderGetRawXmax(tuple);
-           if (MultiXactIdPrecedes(multi, cutoff_multi))
+           if (TransactionIdPrecedes(HeapTupleGetUpdateXid(tuple),
+                                     cutoff_xid))
                return true;
        }
    }
+   else
+   {
+       xid = HeapTupleHeaderGetRawXmax(tuple);
+       if (TransactionIdIsNormal(xid) &&
+           TransactionIdPrecedes(xid, cutoff_xid))
+           return true;
+   }
 
    if (tuple->t_infomask & HEAP_MOVED)
    {