DBRaven
Failure Mode · capacity

Schema Migration Lock

critical

Summary

An ALTER TABLE or other DDL statement takes an ACCESS EXCLUSIVE lock that conflicts with every other lock type, including a plain SELECT's ACCESS SHARE. Once that lock request is waiting, every later query on the table queues behind it too, so a DDL statement that is merely waiting, not yet running, is enough to take the table's entire traffic down.

Description

PostgreSQL DDL statements take the most restrictive lock type, ACCESS EXCLUSIVE, which conflicts with all other lock modes. PostgreSQL's lock queue is first-in-first-out per table: once the DDL's lock request is queued, every subsequent request on that table, including plain SELECTs, queues behind it too, even while the DDL itself is still waiting and has not started running. The DDL is not skipped or deprioritized; it holds its place in line and blocks everything arriving after it for as long as it waits plus however long it then takes to run.

The failure mechanism: ALTER TABLE ADD COLUMN NOT NULL DEFAULT 'value' is issued on a table with 500 million rows while one long-running SELECT has held the table for 5 minutes. The ALTER waits for that SELECT to finish, and while it waits, every new SELECT, INSERT, UPDATE, and DELETE on the table queues behind it. The application's connection pool fills with queries waiting on the table lock; within roughly 60 seconds the pool is exhausted, and within roughly 120 seconds the feature area backed by that table is effectively down, entirely because of a DDL statement that has not even started executing yet.

PostgreSQL 11 removed the table rewrite for adding a column with a non-volatile default, but the ACCESS EXCLUSIVE lock is still acquired and must still wait for every active transaction on the table to finish first; a single long-running analytics query or a replica catching up can still queue the DDL for minutes. Table rewrites that are still required (adding NOT NULL without a default pre-PG11, changing a column's type, adding a constraint with validation against existing rows) on 100M+ row tables can take 30 to 60 minutes fully locked if not run through an online schema change tool.

Two PostgreSQL-specific escapes exist for the two different expensive parts of a migration, the lock wait and the row-by-row work, and they generalize the same idea: do the expensive part without holding ACCESS EXCLUSIVE, then take the exclusive lock only for a fast final step. CREATE INDEX CONCURRENTLY builds an index without taking ACCESS EXCLUSIVE, at the cost of 2 to 10x the build time and two full table scans instead of one; because of how it tracks visibility across those two scans it cannot run inside a transaction block at all, which matters because migration frameworks (Alembic, Flyway, Rails migrations) commonly wrap each migration in one transaction by default, so a CONCURRENTLY statement needs that wrapping explicitly disabled or the migration will fail outright before it even reaches the lock. Adding a constraint (CHECK or foreign key) as NOT VALID takes ACCESS EXCLUSIVE only briefly to record the constraint, deferring the expensive scan that checks existing rows against it to a separate VALIDATE CONSTRAINT statement, which takes only SHARE UPDATE EXCLUSIVE, a lock that does not block ordinary reads and writes. Both techniques split one expensive, fully-locking operation into a cheap locking step and an expensive non-locking step.

Lock queue priority does not exist in PostgreSQL: a DDL statement that has waited 10 minutes has no priority over a query that arrives in the 11th minute; both simply wait their turn in arrival order.

Characteristics

Propagationfan out
Time to detectDetectable within seconds via pg_locks monitoring. Application impact (latency, errors) becomes visible within 30 to 120 seconds depending on connection pool size. If lock_timeout is unset (effectively 0, meaning wait forever), the DDL blocks indefinitely.
Blast radiusAll reads and writes to the affected table are blocked for the lock wait plus DDL execution time. Tables with foreign key relationships can also be affected if the DDL triggers a lock on the referenced table. Connection pool exhaustion is likely within 1 to 3 minutes as application threads queue on the blocked table. If multiple services share the database, all are affected simultaneously.

Triggers

  • ·ALTER TABLE on a table with over 1M rows in production without an online schema change tool
  • ·Adding a NOT NULL constraint on a large table without splitting into: ADD COLUMN nullable, UPDATE, ADD CONSTRAINT NOT NULL
  • ·CREATE INDEX without CONCURRENTLY on a large table
  • ·A migration framework (Alembic, Flyway, Liquibase) running migrations at application startup with no awareness of lock impact
  • ·A long-running analytics query active on the table when a migration is triggered

Detection Signals

disk saturationerror rate spike

Mitigation Strategies

Always set lock_timeout before running migrationscomplexity: low

SET lock_timeout = '5s' before any DDL statement. If the lock cannot be acquired within 5 seconds, the statement aborts with an error instead of queuing and blocking every subsequent query. Retry during a lower-traffic window or add statement-level retry logic.

Use CREATE INDEX CONCURRENTLY for new indexes on live tablespreventscomplexity: low

CREATE INDEX CONCURRENTLY does not take ACCESS EXCLUSIVE, letting reads and writes continue throughout, at the cost of 2 to 10x the build time. It cannot run inside a transaction block, so migration tooling that wraps migrations transactionally by default needs that disabled for this statement specifically. If it fails mid-build, an invalid index is left behind and must be cleaned up with DROP INDEX CONCURRENTLY.

Add constraints as NOT VALID, then VALIDATE CONSTRAINT separatelypreventscomplexity: low

ALTER TABLE ... ADD CONSTRAINT ... CHECK (...) NOT VALID takes ACCESS EXCLUSIVE only briefly, to record the constraint without checking existing rows. A following ALTER TABLE ... VALIDATE CONSTRAINT does the expensive existing-row scan under SHARE UPDATE EXCLUSIVE instead, which does not block ordinary reads and writes. New rows are checked against the constraint immediately; only the historical-row check is deferred and de-locked.

Decompose NOT NULL additions into multiple backward-compatible stepspreventscomplexity: medium

Step 1: ADD COLUMN new_col TEXT (nullable, no lock impact on existing rows). Step 2: UPDATE table SET new_col = 'default' WHERE new_col IS NULL, in batches of 10,000 rows. Step 3: ALTER TABLE ALTER COLUMN new_col SET NOT NULL (a lock is still taken, but briefly, since every row already has a value). This separates the expensive data migration from the lock-acquiring schema change.

Use pg_repack or pt-online-schema-change for table rewritespreventscomplexity: high

pg_repack performs a table rewrite online without ACCESS EXCLUSIVE for most of the operation: it builds a new table, copies data in the background, and does a fast swap at the end. The swap itself briefly takes ACCESS EXCLUSIVE, typically under a second. Suited to long-running migrations (column type changes, adding a column with a default on pre-PG11 versions).

Schedule migrations during low-traffic windows with a reduced connection poolcomplexity: medium

Temporarily reduce the application connection pool size before running the migration, bounding how many queries can queue behind the DDL, and run during the lowest-traffic window available. Still set lock_timeout regardless, to bound the worst case.

Recovery Steps

  1. 1.Check whether the migration is queued (waiting) or running: SELECT pid, query, wait_event FROM pg_stat_activity WHERE state = 'active'
  2. 2.If the migration is queued and has not started, terminate it immediately with pg_cancel_backend(pid) to unblock the queue
  3. 3.If the migration is running (a table rewrite), assess whether to kill it (a partial rollback) or let it finish
  4. 4.Once the blocking query is removed, connection queues drain within 30 to 60 seconds
  5. 5.Reschedule the migration with lock_timeout set, during a low-traffic window
  6. 6.Implement online schema change tooling (CONCURRENTLY, NOT VALID plus VALIDATE, pg_repack, decomposed steps) before retrying

Estimated recovery time: Seconds once the blocking DDL is terminated. If the DDL was running a table rewrite, terminating it triggers a full rollback of the partial rewrite; on a 500M-row table that rollback alone can take 10 to 30 minutes.

Affected Systems

Patterns

shardingevent sourcingoutbox pattern

Technologies

postgresqlmysql

Basis

Precisely specified PostgreSQL locking behavior; the ACCESS EXCLUSIVE lock queue mechanics, the CONCURRENTLY-cannot-run-in-a-transaction restriction, and the NOT VALID plus VALIDATE CONSTRAINT lock-splitting technique are documented PostgreSQL DDL behavior and reproducible under the stated conditions.

Run This Failure

Blast radius analysis for this failure mode within each scenario that carries it.

Related Architecture Knowledge

Outbound: this entity affects

Introduces RiskWorkload
write heavy transactional
Grounded

Schema migrations on write-heavy transactional tables acquire aggressive locks (AccessExclusiveLock) that block all reads and writes. On a high-traffic table receiving 5,000 writes/second, a migration lock that waits even 1 second queues 5,000 transactions behind it, causing a connection pool exhaustion cascade.

Tradeoffs

  • ·Online schema change tools (gh-ost, pg_repack) add operational complexity but eliminate downtime risk
  • ·lock_timeout=100ms causes migration to fail rather than block: requires retry logic
  • ·{'Zero-downtime migrations require more code': 'add new column → backfill → add constraint → drop old column'}
Full relationship →

Used In Architecture Scenarios

Developer Tools Platformhigh

Multi-Tenant SaaS

A multi-tenant developer tooling platform providing CI/CD pipeline execution, log aggregation, code analysis, and dependency scanning across isolated tenant organizations. Tenant isolation is the primary correctness constraint: a security boundary violation between tenants is a critical incident, not a performance event. PostgreSQL row-level security enforces data isolation; Redis manages job queues and distributed locks; Elasticsearch indexes pipeline log output for search; Kafka delivers webhook events to tenant-registered endpoints; MinIO stores pipeline artifacts. Resource quota enforcement prevents any single tenant's burst from affecting others.

Financial Ledger Platformexpert

Financial Ledger

A strict consistency financial architecture for double-entry accounting, payment processing, and audit trail management where every debit must have a corresponding credit, every state transition must be ordered and durable, and the complete history must be reconstructable without gaps. Event sourcing provides an append-only audit log; the outbox pattern guarantees at-least-once downstream event delivery without distributed two-phase commit (idempotent consumers deduplicate on event ID for effectively-once processing); PostgreSQL provides ACID guarantees for ledger entry atomicity.

Healthcare Records Platformexpert

Financial Ledger

An electronic health record (EHR) architecture built around strict auditability, HIPAA compliance, and append-only correctness. Clinical records are mutable by design (amendments, addenda) but corrections must be explicitly attributed, not silently overwritten. Event sourcing provides a reconstructable audit log; PostgreSQL row-level security enforces patient-level access control at the database layer; Kafka streams HL7 FHIR events to downstream clinical systems. The architecture must support breach detection, access auditing, and state reconstruction at any historical point: not just current state retrieval.