Home / Databases / PostgreSQL

DatabasesTroubleshootingAdvanced

PostgreSQL replica: 'canceling statement due to conflict with recovery' - causes, settings and trade-offs

Why long reports on a PostgreSQL hot standby get cancelled, how to read pg_stat_database_conflicts, and how to choose between max_standby_streaming_delay, hot_standby_feedback and vacuum truncation settings without bloating the primary.

Deepak Bhardwaj4 min read

Applies toPostgreSQL 12-17 streaming replication (self-managed, Azure Database for PostgreSQL Flexible Server, Amazon RDS/Aurora PostgreSQL read replicas)

Symptom

Reports or ETL queries on a read replica fail after running for a while:

ERROR:  canceling statement due to conflict with recovery
DETAIL:  User query might have needed to see row versions that must be removed.

Other DETAIL lines you may see:

  • User was holding a relation lock for too long.
  • User was holding shared buffer pin for too long.
  • User transaction caused buffer deadlock with recovery.

Why it happens

The standby replays WAL from the primary. Sometimes the next WAL record would remove data a running query still needs, or needs a lock the query holds. PostgreSQL waits up to max_standby_streaming_delay (default 30 s), then cancels the query so replication can continue.

Main sources of conflicts:

Conflict type Cause on the primary
Snapshot VACUUM removes dead row versions the standby query still needs
Lock An ACCESS EXCLUSIVE lock: DDL, TRUNCATE, or VACUUM truncating empty pages at the end of a table
Buffer pin Replay needs a page the query has pinned
Tablespace / database Dropping a tablespace or database in use

Step 1: count conflicts by type (on the standby)

SELECT datname, confl_snapshot, confl_lock, confl_bufferpin, confl_deadlock, confl_tablespace
FROM pg_stat_database_conflicts
WHERE datname = current_database();

Counters are cumulative. Sample twice, an hour apart, to see which type is growing. On PostgreSQL 16+ there's also confl_active_logicalslot.

Step 2: pick the right fix for the conflict type

Snapshot conflicts: three options

A. Let the standby wait longer.

-- On the standby (postgresql.conf or server parameters); reload is enough
ALTER SYSTEM SET max_standby_streaming_delay = '15min';
SELECT pg_reload_conf();

Effect: queries survive longer, but replication lag grows while replay waits. Fine for a dedicated reporting replica; bad for an HA standby you fail over to.

B. Tell the primary not to remove rows the standby needs.

ALTER SYSTEM SET hot_standby_feedback = on;   -- on the standby
SELECT pg_reload_conf();

Effect: the primary's VACUUM keeps rows that standby queries still see. Cancellations mostly stop, but long standby queries now cause bloat on the primary, the same as a long transaction there. Watch it:

-- On the primary: how old is the xmin the standby is holding back?
SELECT application_name, backend_xmin, age(backend_xmin) AS xmin_age
FROM pg_stat_replication;

If xmin_age keeps growing into the hundreds of millions, a standby query is holding back vacuum. Also cap statement duration on the replica (statement_timeout for the reporting role) so one runaway report can't bloat the primary.

C. Split the workloads. Keep the HA standby strict (default delay, feedback off), and add a separate reporting replica with a long delay or with feedback on. This is the cleanest answer when you have both needs.

Lock conflicts

Often caused by VACUUM truncating empty pages at the end of a table, which needs an ACCESS EXCLUSIVE lock that's replayed on the standby. For big, frequently-vacuumed tables you can disable truncation:

ALTER TABLE events SET (vacuum_truncate = false);

Other lock sources: DDL during business hours, TRUNCATE, partition detach/drop. Schedule them outside the reporting window.

Buffer pin conflicts

Usually rare and short. If they grow, look for very long cursors or FETCH loops on the standby that hold pages pinned.

Managed services

Service Notes
Azure Database for PostgreSQL Flexible Server Set max_standby_streaming_delay and hot_standby_feedback as server parameters on the replica
Amazon RDS PostgreSQL Set in the replica's parameter group; hot_standby_feedback is supported
Aurora PostgreSQL Replicas share storage, so the mechanics differ: long queries on readers can still be cancelled or hold back cleanup; check Aurora's aurora_* replica lag metrics and docs

Decision table

Replica role Recommended settings
HA standby only Defaults; no long queries there
Reporting only max_standby_streaming_delay = longest report + margin, or feedback on with a statement timeout
Both on one replica Split into two replicas if you can; otherwise feedback on + strict timeouts + bloat monitoring

Prevent it next time