This document provides a detailed analysis of PostgreSQL lock behavior and downtime risks identified in the Nova-Rewards migration audit. Understanding these lock patterns is critical for implementing zero-downtime deployments in production environments.
PostgreSQL uses Multi-Version Concurrency Control (MVCC) with various lock levels that determine concurrent access patterns:
| Lock Mode | Abbreviation | Blocks | Allows | Use Case |
|---|---|---|---|---|
| ACCESS SHARE | AS | None | All reads/writes | SELECT queries |
| ROW SHARE | RS | ACCESS EXCLUSIVE | Most operations | SELECT FOR UPDATE |
| ROW EXCLUSIVE | RX | SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE | SELECT, INSERT, UPDATE, DELETE | DML operations |
| SHARE UPDATE EXCLUSIVE | SUE | SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE | SELECT, INSERT, UPDATE, DELETE | VACUUM, CREATE INDEX CONCURRENTLY |
| SHARE | S | ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE | SELECT | CREATE INDEX |
| SHARE ROW EXCLUSIVE | SRE | ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE | SELECT | Some ALTER TABLE operations |
| EXCLUSIVE | E | ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE | SELECT | Some DDL operations |
| ACCESS EXCLUSIVE | AE | ALL OTHER LOCKS | NOTHING | Most DDL operations |
Impact: Blocks ALL concurrent operations (reads and writes) Duration: Until transaction commits or rolls back Production Risk: Complete application downtime
-
ALTER TABLE ADD COLUMN with DEFAULT
- Requires rewriting entire table to set default values
- Lock duration proportional to table size
- Estimated downtime: 1-10 seconds per million rows
-
ALTER TABLE ALTER COLUMN TYPE
- Triggers complete table rewrite
- Most dangerous operation in our audit
- Estimated downtime: 10-300 seconds for large tables
-
CREATE INDEX (without CONCURRENTLY)
- Scans entire table to build index
- 32 migrations affected in our audit
- Estimated downtime: 5-60 seconds per index
-
ALTER TABLE ADD CONSTRAINT
- Must verify constraint across entire table
- Lock duration depends on validation complexity
-
CREATE TRIGGER
- Requires exclusive access to modify table metadata
- Usually fast but blocks all access
-- CRITICAL: Causes complete table rewrite
ALTER TABLE point_transactions DROP COLUMN amount;
ALTER TABLE point_transactions RENAME COLUMN amount_int TO amount;PostgreSQL Behavior:
- Lock: ACCESS EXCLUSIVE for entire operation
- Process:
- Creates new table with modified schema
- Copies all existing data row-by-row
- Rebuilds all indexes
- Updates system catalogs
- Drops old table
- Blocking: All SELECT, INSERT, UPDATE, DELETE operations
- Duration: Proportional to table size (could be minutes for large tables)
- MVCC Impact: No concurrent transactions can proceed
-- CRITICAL: Column type change triggers rewrite
ALTER TABLE webhooks ALTER COLUMN secret TYPE TEXT;
ALTER TABLE users ALTER COLUMN email TYPE TEXT;PostgreSQL Behavior:
- Lock: ACCESS EXCLUSIVE
- Process: Table rewrite required when changing column type
- Risk Level: CRITICAL for users table (core application data)
ALTER TABLE transactions DROP CONSTRAINT IF EXISTS transactions_tx_type_check;
ALTER TABLE transactions ALTER COLUMN from_wallet DROP NOT NULL;
ALTER TABLE transactions ALTER COLUMN to_wallet DROP NOT NULL;
ALTER TABLE transactions ALTER COLUMN merchant_id DROP NOT NULL;
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS user_id INTEGER REFERENCES users(id);
-- ... more operationsPostgreSQL Behavior:
- Lock: ACCESS EXCLUSIVE for each operation
- Accumulation Effect: Lock time accumulates across multiple operations
- Risk: Extended downtime from sequential lock acquisitions
Found in 32 migrations, examples:
-- UNSAFE: Acquires ACCESS EXCLUSIVE lock
CREATE INDEX IF NOT EXISTS idx_campaigns_merchant_id ON campaigns (merchant_id);
CREATE INDEX IF NOT EXISTS idx_transactions_from_wallet ON transactions (from_wallet);PostgreSQL Behavior:
- Lock: ACCESS EXCLUSIVE
- Process:
- Scans entire table to build index
- Sorts key values
- Writes index pages to disk
- Updates system catalogs
- Duration: Proportional to table size and index complexity
- Blocking: All table access during creation
Safe Alternative:
-- SAFE: Uses SHARE UPDATE EXCLUSIVE lock
CREATE INDEX CONCURRENTLY idx_campaigns_merchant_id ON campaigns (merchant_id);Examples from multiple migrations:
-- UNSAFE: Rewrites entire table
ALTER TABLE campaigns ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW();
ALTER TABLE users ADD COLUMN IF NOT EXISTS notification_preferences JSONB NOT NULL DEFAULT '{"rewards":true}';PostgreSQL Behavior:
- Lock: ACCESS EXCLUSIVE
- Process:
- Adds column metadata
- Rewrites entire table to populate default values
- Rebuilds all indexes that don't explicitly exclude the column
- MVCC Impact: All existing snapshots become invalid
Safe Alternative (Three-Phase Pattern):
-- Phase 1: Add nullable column (fast)
ALTER TABLE campaigns ADD COLUMN updated_at TIMESTAMPTZ;
-- Phase 2: Backfill in batches (can be done during low traffic)
UPDATE campaigns SET updated_at = NOW() WHERE updated_at IS NULL;
-- Phase 3: Add NOT NULL constraint (fast)
ALTER TABLE campaigns ALTER COLUMN updated_at SET NOT NULL;Migration: 019_field_level_encryption.sql modifies users table
Application Query: SELECT * FROM users WHERE wallet_address = $1
Result: Query blocks until migration completes (potentially minutes)
User Impact: Login timeout, session failures
Migration: Creating index on transactions table
Application Query: INSERT INTO transactions (...) VALUES (...)
Result: Transaction processing halts completely
Business Impact: Payment processing downtime
Migration: Adding columns to campaigns table
Application Query: UPDATE campaigns SET is_active = false WHERE id = $1
Result: Campaign management interface freezes
Admin Impact: Cannot manage active campaigns
PostgreSQL's MVCC system creates transaction snapshots, but ACCESS EXCLUSIVE locks break this isolation:
- Active Transactions: Any transaction that started before the migration will block
- New Transactions: Cannot start until lock is released
- Connection Pooling: Pool exhaustion as connections wait for locks
- Cascading Failures: Backend services timeout waiting for database responses
Migrations that acquire multiple locks can create deadlock opportunities:
-- Migration acquires lock on table A, then table B
-- Application transaction holds lock on table B, needs table A
-- Result: Deadlock, one transaction must be killed-- Instead of: CREATE INDEX idx_name ON table (column);
CREATE INDEX CONCURRENTLY idx_name ON table (column);Lock: SHARE UPDATE EXCLUSIVE (allows reads and writes) Limitation: Cannot be used inside transaction blocks
-- Phase 1: Add nullable column
ALTER TABLE table ADD COLUMN new_col TYPE;
-- Phase 2: Backfill (batched updates during low traffic)
UPDATE table SET new_col = default_value WHERE new_col IS NULL;
-- Phase 3: Add constraint
ALTER TABLE table ALTER COLUMN new_col SET NOT NULL;-- Instead of: ALTER TABLE table ALTER COLUMN col TYPE new_type;
-- Use: Add new column, migrate data, drop old column
ALTER TABLE table ADD COLUMN col_new new_type;
UPDATE table SET col_new = col::new_type; -- batched
ALTER TABLE table DROP COLUMN col;
ALTER TABLE table RENAME COLUMN col_new TO col;- Active Connections:
SELECT count(*) FROM pg_stat_activity - Waiting Queries:
SELECT * FROM pg_stat_activity WHERE waiting = true - Lock Waits:
SELECT * FROM pg_locks WHERE NOT granted - Transaction Age: Check for long-running transactions that could block migrations
-- Set statement timeout to prevent indefinite waits
SET statement_timeout = '30s';
SET lock_timeout = '10s';The current migration set poses significant production risks due to:
- Systematic use of ACCESS EXCLUSIVE operations (32 migrations)
- Table rewrite operations that could cause minutes of downtime
- Multiple schema changes that accumulate lock time
- Critical table modifications (users, transactions) without safe patterns
Implementing the recommended three-phase patterns and CONCURRENTLY options is essential for zero-downtime deployments.