diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,26 @@
 # Changelog
 
+## [0.5.2.0] - 2026-09-26
+
+### Fixed
+
+* A read against the standby is no longer destroyed by a vacuum on the
+  primary. The standby was configured to serve reads and to stay deliberately
+  behind, and then left to postgres' defaults on what should happen when
+  recovery met one of those reads: the primary was never told what a standby
+  reader was holding, so it was free to remove those row versions, and
+  replaying the cleanup cancelled the reader. The read came back as `SQLSTATE
+  40001`, or, when the session was holding a snapshot, as a dropped
+  connection.
+
+  The standby now sends `hot_standby_feedback`, so the primary keeps what the
+  standby's readers still need, and reports it often enough to matter for
+  databases that live as briefly as these do. Recovery also waits for a
+  conflicting read rather than cancelling it, which covers the conflicts
+  feedback does not: a cleanup already in flight, or a lock that replayed DDL
+  wants. A replica in a test suite is there to be behind, and a test that
+  wants it caught up says so with `awaitReplica`.
+
 ## [0.5.1.0] - 2026-09-14
 
 ### Changed
diff --git a/src/Test/Syd/Persistent/Postgresql.hs b/src/Test/Syd/Persistent/Postgresql.hs
--- a/src/Test/Syd/Persistent/Postgresql.hs
+++ b/src/Test/Syd/Persistent/Postgresql.hs
@@ -270,7 +270,27 @@
           [ [ "port = " ++ show primaryPort,
               "unix_socket_directories = '" ++ socketDir ++ "'",
               "listen_addresses = ''",
-              "hot_standby = on"
+              "hot_standby = on",
+              -- Tell the primary what this standby's readers still need, so
+              -- it keeps those row versions instead of vacuuming them away
+              -- and leaving recovery to cancel the reader that wanted them.
+              -- The base backup took a replication slot, which is what
+              -- carries the feedback, so this has somewhere to be recorded.
+              "hot_standby_feedback = on",
+              -- Feedback is only sent this often, and the default of ten
+              -- seconds is longer than the databases here live, so without
+              -- this the primary would learn what to keep long after it had
+              -- already thrown it away.
+              "wal_receiver_status_interval = 1s",
+              -- Feedback closes the window rather than the door: it cannot
+              -- retract a cleanup already in flight, and it says nothing
+              -- about the conflicts that are not about row versions at all,
+              -- such as a lock a replayed DDL wants. Recovery waiting is the
+              -- right answer to those here, because a replica in a test
+              -- suite is there to be behind, and a test that wanted it
+              -- caught up says so with 'awaitReplica'.
+              "max_standby_streaming_delay = -1",
+              "max_standby_archive_delay = -1"
             ],
             ["recovery_min_apply_delay = '" ++ renderReplicaLag replicaConfigLag ++ "'"]
           ]
diff --git a/sydtest-persistent-postgresql.cabal b/sydtest-persistent-postgresql.cabal
--- a/sydtest-persistent-postgresql.cabal
+++ b/sydtest-persistent-postgresql.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           sydtest-persistent-postgresql
-version:        0.5.1.0
+version:        0.5.2.0
 synopsis:       An persistent-postgresql companion library for sydtest
 category:       Testing
 homepage:       https://github.com/NorfairKing/sydtest#readme
diff --git a/test/Test/Syd/Persistent/Postgresql/StandbySpec.hs b/test/Test/Syd/Persistent/Postgresql/StandbySpec.hs
--- a/test/Test/Syd/Persistent/Postgresql/StandbySpec.hs
+++ b/test/Test/Syd/Persistent/Postgresql/StandbySpec.hs
@@ -4,6 +4,7 @@
 module Test.Syd.Persistent.Postgresql.StandbySpec (spec) where
 
 import Database.Persist
+import Database.Persist.Sql (rawExecute, runSqlPoolNoTransaction)
 import Database.PostgreSQL.Simple (SqlError (..))
 import Test.Syd
 import Test.Syd.Persistent.Example
@@ -42,3 +43,39 @@
         awaitReplica unreplicated
         mPerson <- onReplica unreplicated $ get i
         mPerson `shouldBe` Just p
+
+      -- A read on the standby holds a snapshot.  If the primary is free to
+      -- remove the row versions that snapshot needs, replaying the cleanup
+      -- leaves the standby no choice but to cancel the read, and the
+      -- application sees SQLSTATE 40001 rather than its data.
+      --
+      -- Nothing the application does can avoid that, so the standby has to
+      -- tell the primary what it is holding.
+      it "keeps a read on the replica alive while the primary vacuums what it is reading" $ \pools -> do
+        let people = [Person {personName = "Vacuumed " ++ show i, personAge = Just i} | i <- [1 :: Int .. 100]]
+        _ <- onPrimary pools $ insertMany people
+        awaitReplica pools
+
+        onReplica pools $ do
+          -- Read committed takes a new snapshot per statement and lets the
+          -- old one go, so there would be nothing held across the cleanup.
+          -- An application that reads twice and compares needs this.
+          rawExecute "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ" []
+
+          -- Take the snapshot the cleanup below has to be kept away from.
+          firstRead <- selectList ([] :: [Filter Person]) []
+          liftIO $ length firstRead `shouldBe` 100
+
+          liftIO $ do
+            -- Every row gets a new version, so every old version is garbage,
+            -- and the vacuum writes the cleanup that conflicts.
+            onPrimary pools $ updateWhere ([] :: [Filter Person]) [PersonAge =. Just 0]
+            -- VACUUM refuses to run inside a transaction block.
+            runSqlPoolNoTransaction
+              (rawExecute "VACUUM person" [])
+              (replicatedPoolsPrimary pools)
+              Nothing
+            awaitReplica pools
+
+          secondRead <- selectList ([] :: [Filter Person]) []
+          liftIO $ length secondRead `shouldBe` 100
