We ran heavy analytics on a read replica to keep load off the primary. It worked until the queries got long, and then they started dying with canceling statement due to conflict with recovery. A report that took three minutes had maybe a fifty-fifty chance of finishing.
Someone found hot_standby_feedback, turned it on, and the cancellations vanished. A week later the primary's tables were bloating and nobody connected the two events. They are absolutely connected, and the link is the most important trade-off in PostgreSQL replication.
Why replicas cancel queries
A replica replays the primary's write-ahead log, including the cleanup that VACUUM performs. If the primary vacuums away a row version, that cleanup arrives at the replica and must be applied. But a long-running query on the replica might still need that old row version for its snapshot.
PostgreSQL resolves the conflict by waiting up to max_standby_streaming_delay and then cancelling the query so replay can proceed. That is the recovery-conflict cancellation: the replica chooses staying current over letting your long query finish.
What hot_standby_feedback does
Turn on hot_standby_feedback and the replica tells the primary which old row versions its running queries still need. The primary then holds back its vacuum horizon so it does not remove those versions yet. With nothing being cleaned out from under them, the replica's long queries stop getting cancelled.
It is a clean fix for the symptom. The replica reports its oldest needed xmin upstream, and the primary respects it.
-- On the replica
ALTER SYSTEM SET hot_standby_feedback = on;
SELECT pg_reload_conf();
-- On the primary: see the horizon a replica is holding back
SELECT application_name, backend_xmin
FROM pg_stat_replication;
The cost lands on the primary
Holding back the vacuum horizon means the primary keeps dead tuples it would otherwise have removed. A long query on the replica now directly delays cleanup on the primary, so the primary's tables accumulate dead rows and bloat — exactly the bloat that appeared a week after the setting was flipped.
In effect, hot_standby_feedback moves the pain from 'queries cancelled on the replica' to 'bloat on the primary.' Neither is free; you are choosing which one you can tolerate.
Tuning the trade-off
You have a spectrum, not a binary. With feedback off, raise max_standby_streaming_delay on the replica to give queries more grace before cancellation — good if occasional bloat-free cancellations are acceptable. With feedback on, keep replica queries short and watch primary bloat closely. Many teams run feedback on but enforce a statement_timeout on the replica so one runaway query cannot pin the primary's horizon indefinitely.
Monitor backend_xmin in pg_stat_replication to see how far back a replica is holding the primary, and alert if it lags too far. That number is the bloat you are signing up for.
- Replicas cancel long queries to keep replaying the primary's vacuum cleanup.
- hot_standby_feedback stops cancellations by holding back the primary's vacuum horizon.
- The cost is dead tuples and bloat accumulating on the primary.
- Cap replica query length (statement_timeout) so it cannot pin the horizon forever.
- Watch backend_xmin in pg_stat_replication to see the horizon being held.
The practical standard
The best PostgreSQL performance work is boring in the right way. Name the failure mode, capture the before plan or metric, make one change, and compare the exact same signal afterward. Anything else is just a more confident guess.