Reproduced lab · PostgreSQL 17

One idle transaction. A waiting migration. Then reads stop.

A queued DDL lock stopped eight test readers for 14 seconds. Follow the captured blocking chain and the measured timeout response.

Lab disclosure: Synthetic workload and generated data in an isolated PostgreSQL 17 environment. This is a measured reproduction, not a client incident.

The trigger and symptom

Eight clients ran primary-key reads against a 200,000-row orders table. Five seconds into the run, another session selected from the table and then sat idle inside its transaction. At ten seconds, a deployment issued a catalog-only ALTER TABLE orders ADD COLUMN note text. The operation needed an AccessExclusiveLock and waited behind the open transaction.

Reader throughput fell to zero from seconds 11 through 24. The DDL itself took 14.96 seconds because it waited for the old transaction to commit at second 25. This is the kind of incident where query plans remain healthy while an ordinary endpoint appears to freeze after a migration begins.

What PostgreSQL showed during the queue

SessionState / statementRelation lockGranted?Blocked by
A · pid 252Idle in transactionAccessShareLockYes—
B · pid 259ALTER TABLE waitingAccessExclusiveLockNoA
Eight readersPrimary-key SELECT waitingAccessShareLockNoB

This is the snapshot taken four seconds after the DDL was issued. PIDs and exact timings vary by run. The repository's labs/ddl-lock-queue folder contains the setup, workload script and captured output.

The idle reader did not directly block the later readers. The pending AccessExclusiveLock request sat between them in the lock queue. pg_blocking_pids() showed the DDL blocked by the idle transaction, and every reader blocked by the DDL. PostgreSQL's lock-mode documentation explains why an exclusive request conflicts with ordinary reads.

The measured response

The lab was repeated with lock_timeout = '2s' set only for the migration session. The DDL failed after 2.01 seconds instead of waiting nearly 15 seconds, and reader throughput recovered by second 13. The timed-out migration still needed to be retried later. A bounded timeout reduced the queue's duration; it did not make the schema change unnecessary or guarantee a safe deployment.

Response in a production incident

  1. Capture the blocking graph and transaction age while the event is active; identify the oldest blocker and the owner of its session.
  2. Coordinate with the migration owner. If canceling the waiting DDL is safe, it may release later readers. Do not terminate the blocker blindly; it may be doing essential work.
  3. For the next migration, test the lock mode, keep transactions short and use a bounded migration-session lock_timeout with a deliberate retry path.
  4. Validate with reader throughput, endpoint latency and lock-wait signals after the queue clears. A faster SELECT plan would not have addressed this incident.

The laptop throughput values are not a production promise. The repeatable result is the blocking chain and the shorter stall when the waiting DDL fails promptly. For the broader diagnostic method, see the PostgreSQL concurrency guide.

Lock contention

Need to explain a production lock queue?

Bring the time window, application symptom and available wait or activity snapshots. The investigation connects the queue to a safe next change.

Prefer to talk first? Book a 20-minute triage call.