ETL Pipeline Lock Contention on Source Database
partialSummary
When a bulk ETL job reads from the production OLTP database, it competes for shared resources: buffer pool, WAL, CPU, and transaction snapshot slots : causing OLTP query latency to increase during the ETL window. Beyond direct I/O contention, an open long-running ETL transaction prevents autovacuum from advancing its horizon, causing table bloat to accumulate and dead tuple count to grow. OLTP reads slowed by bloated tables compound the initial contention, producing a degradation window that outlasts the ETL job itself.
Description
ETL jobs that read directly from OLTP databases create contention on multiple resource dimensions simultaneously. The most immediate is buffer pool pollution: a full-table sequential scan for export reads every page of the target table into shared_buffers, evicting hot OLTP pages (recently-accessed index pages, frequent row lookups). After the ETL scan completes, subsequent OLTP queries that previously hit in-memory pages must now read from storage. For a table with a 10 GB buffer pool footprint and an ETL scan that reads 50 GB, the ETL effectively flushes the OLTP hot set and forces a cold-start on shared_buffers after completion.
The autovacuum blocking effect is more subtle and longer-lasting. PostgreSQL autovacuum cannot reclaim dead tuples that were visible to any open transaction snapshot. An ETL job that holds a transaction open for 30 minutes while scanning holds a snapshot that prevents autovacuum from advancing past the transaction start time. During this 30-minute window, all OLTP DELETE and UPDATE operations generate dead tuples that cannot be reclaimed. For a table with 10,000 updates/minute, 30 minutes produces 300,000 dead tuples that cannot be vacuumed. Post-ETL, autovacuum runs aggressively to catch up, competing with OLTP I/O for the next 10–30 minutes. OLTP queries slow further because they must skip over dead tuples in table pages, reducing effective page density.
Lock escalation is the third failure mode. Some ETL patterns use table-level operations for performance (TRUNCATE + INSERT instead of incremental updates, ALTER TABLE ADD COLUMN to add a computed column, CREATE INDEX CONCURRENTLY that still takes an initial brief lock). These operations request locks that conflict with OLTP AccessShare locks. If the ETL job holds a transaction lock and then requests an exclusive lock (even briefly for a single operation within a longer pipeline), it must wait for all active OLTP transactions to complete before the exclusive lock is granted. Simultaneously, all new OLTP transactions that attempt to acquire AccessShare on the same table are blocked behind the pending exclusive lock, producing a cascading read lockout identical to the materialized view refresh contention failure mode.
Logical replication slots used by CDC pipelines (Debezium, pglogical) create another variant of this problem: an inactive or lagging CDC slot holds a replication slot that prevents WAL from being removed. If the slot falls behind by hours, WAL accumulates without bound, potentially filling the disk and crashing the database entirely.
Characteristics
Triggers
- ·Full-table ETL scan executed during business hours when OLTP traffic is at >50% of peak
- ·ETL job opens a transaction and holds it open for the duration of the scan (not using cursor-based streaming)
- ·CDC replication slot falls behind and accumulates WAL without alerting
- ·ETL pipeline executes schema changes (CREATE INDEX, ALTER TABLE) on the production source without a maintenance window
- ·ETL job uses statement_timeout=0 with no external kill mechanism, allowing runaway scans to run indefinitely
Detection Signals
Mitigation Strategies
Configure the ETL job to connect to a dedicated read replica that receives no OLTP traffic. All buffer pool pollution, CPU consumption, and table scan I/O is isolated to the replica. Autovacuum on the primary is unaffected because the ETL transaction is not open on the primary. The read replica must have acceptable lag for the ETL use case (typically minutes, acceptable for daily export or batch processing workloads).
Replace full-scan ETL reads with cursor-based streaming: DECLARE etl_cursor CURSOR FOR SELECT * FROM large_table ORDER BY id; FETCH 10000 FROM etl_cursor; ... CLOSE etl_cursor. Open the cursor without a surrounding long transaction: use AUTOCOMMIT between batches. This limits the oldest open transaction age to the duration of one batch fetch (seconds rather than minutes), allowing autovacuum to advance normally. Reduces autovacuum blocking from the full ETL duration to near-zero.
Configure an alert when any replication slot's WAL lag exceeds 5 GB (SELECT pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) FROM pg_replication_slots). If a CDC consumer is unavailable for >4 hours and WAL accumulation threatens disk capacity, drop the replication slot (SELECT pg_drop_replication_slot(slot_name)) to allow WAL cleanup. The CDC pipeline must restart from a snapshot when it recovers. Alert at 2 GB to provide response time before the disk fill threshold.
Recovery Steps
- 1.Terminate the long-running ETL transaction via SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE backend_xmin IS NOT NULL AND age(backend_xmin) > interval '5 minutes'
- 2.Monitor autovacuum catching up by querying pg_stat_user_tables n_dead_tup: expect a decrease within 10 minutes after ETL is terminated
- 3.Check for lagging CDC replication slots and evaluate whether to drop and re-establish them
- 4.Schedule autovacuum manually on heavily-bloated tables via SELECT autovacuum_count FROM pg_stat_user_tables
- 5.Redirect the ETL job to a read replica for the next run
Estimated recovery time: 15–60 minutes for OLTP latency to normalize after ETL termination, as autovacuum reclaims accumulated dead tuples. Buffer pool re-warming (hot pages evicted by the ETL scan) requires 5–20 minutes of normal OLTP traffic to restore the hot set.
Affected Systems
Patterns
Technologies
Basis
PostgreSQL autovacuum horizon blocking by long-running transactions is precisely documented in PostgreSQL internals; buffer pool pollution from ETL scans is a well-understood operational pattern; replication slot WAL accumulation is a documented PostgreSQL operational risk