Module A-13·25 min read

Concurrency bugs are almost never about wrong SQL — they are about wrong locking assumptions. The complete PostgreSQL locking matrix.

Module 13 — Locking Internals: Row Locks, Table Locks, and Advisory Locks

What this module covers: Concurrency bugs in production are almost never about wrong SQL — they are about wrong locking assumptions. This module covers the complete PostgreSQL locking matrix from first principles: what every lock mode protects, how row-level locks differ from table-level locks, the precise semantics of FOR UPDATE vs FOR SHARE vs FOR NO KEY UPDATE, how ALTER TABLE can silently queue behind a single open transaction and take down your application, and how advisory locks let you build distributed coordination primitives directly inside Postgres.


Why Locking Is Hard to Reason About

Postgres has multiple, overlapping lock systems that operate at different granularities simultaneously:

  1. Table-level locks — protect the table structure and relation-level operations
  2. Row-level locks — protect individual tuple modifications
  3. Page-level locks — internal, mostly transparent (buffer pins)
  4. Advisory locks — application-defined locks with no automatic semantics

Every DML statement acquires locks at multiple levels simultaneously. An UPDATE acquires a RowExclusiveLock on the table and a row-level exclusive lock on each modified tuple. The two systems interact: some table-level operations must wait for all row-level locks to be released before they can proceed.

Understanding which operations conflict — and which do not — is the difference between a schema migration that completes in 30 seconds and one that causes a 20-minute outage.


Table-Level Lock Modes

Postgres defines 8 table-level lock modes, ordered from weakest to strongest. Each mode conflicts with some modes and is compatible with others.

The Full Lock Matrix

Lock ModeAcquired ByConflicts With
ACCESS SHARESELECTACCESS EXCLUSIVE only
ROW SHARESELECT FOR UPDATE/SHAREEXCLUSIVE, ACCESS EXCLUSIVE
ROW EXCLUSIVEINSERT, UPDATE, DELETESHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE
SHARE UPDATE EXCLUSIVEVACUUM, ANALYZE, CREATE INDEX CONCURRENTLY, ALTER TABLE ... VALIDATESHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE
SHARECREATE INDEX (non-concurrent)ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE
SHARE ROW EXCLUSIVECREATE TRIGGER, some ALTER TABLEROW EXCLUSIVE and above
EXCLUSIVERare — some replication operationsEverything except ACCESS SHARE
ACCESS EXCLUSIVEALTER TABLE, DROP TABLE, TRUNCATE, VACUUM FULL, LOCK TABLEEverything including SELECT

The critical insight: ACCESS EXCLUSIVE conflicts with every other lock mode, including plain SELECT. This is why ALTER TABLE on a busy table causes an outage.

The ALTER TABLE Outage Pattern

This is one of the most common causes of production incidents. Here is the exact mechanism:

sql

The ALTER TABLE does not hold a lock — it is waiting for one. But its waiting position in the lock queue blocks all subsequent requests, even ones that would normally be compatible with each other.

sql

The Correct ALTER TABLE Pattern

sql

The window between killing queries and running the ALTER must be short — new long-running queries can start in the gap.

Lock Modes in Practice

sql

Row-Level Locks

Table-level locks protect the schema. Row-level locks protect individual tuples from concurrent modification. They are separate systems.

The Four Row Lock Modes

FOR UPDATE

  • Strongest row lock. Marks the row as being updated.
  • Blocks: other FOR UPDATE, FOR NO KEY UPDATE, FOR SHARE, FOR KEY SHARE
  • Use when: you intend to update or delete the row, or you need to prevent any concurrent modification

FOR NO KEY UPDATE

  • Like FOR UPDATE but does not block FOR KEY SHARE
  • Use when: updating non-key columns (foreign keys to this row remain unblocked)
  • Postgres uses this internally for UPDATE statements that don't modify key columns

FOR SHARE

  • Prevents other transactions from updating or deleting the row, but allows other shared locks
  • Blocks: FOR UPDATE, FOR NO KEY UPDATE
  • Does not block: other FOR SHARE, FOR KEY SHARE
  • Use when: reading a row that must not change for the duration of your transaction (e.g., verifying a foreign key reference)

FOR KEY SHARE

  • Weakest row lock. Prevents deletion or key-column updates only.
  • Blocks: FOR UPDATE, FOR NO KEY UPDATE
  • Does not block: FOR SHARE, other FOR KEY SHARE
  • Postgres uses this automatically for foreign key checks
sql

FOR UPDATE — The Correct Pattern

sql

The FOR UPDATE lock prevents any other transaction from modifying this row between your SELECT and your UPDATE. Without it, two concurrent debits can both read the same balance and both proceed.

But note: with PG18's RETURNING OLD/NEW (Module 9), this two-step pattern is often unnecessary. Prefer atomic single-statement operations where possible.

SKIP LOCKED: Non-Blocking Queue Processing

SKIP LOCKED skips rows that are currently locked by another transaction. This enables efficient work queue patterns:

sql

Without SKIP LOCKED, multiple workers queue up on the same row. With SKIP LOCKED, each worker instantly claims a different row. This is the correct pattern for implementing a job queue in Postgres.

NOWAIT: Fail Fast on Lock Contention

sql

NOWAIT is preferable to waiting when the application can handle the retry — it keeps connection hold time short and prevents lock queue buildup.


Deadlocks

A deadlock occurs when two transactions each hold a lock the other needs.

sql

Postgres detects deadlocks automatically (every deadlock_timeout milliseconds, default 1s) and terminates one transaction:

text

Preventing deadlocks: always acquire locks in the same order across all code paths. If every transaction locks row IDs in ascending order, deadlocks are impossible.

sql
sql

Advisory Locks: Application-Level Distributed Locks

Advisory locks are cooperative locks with no automatic semantic — Postgres acquires and releases them only when you explicitly call the advisory lock functions. They are:

  • Lightweight (stored in shared memory, not in system tables)
  • Scoped to either a transaction or a session
  • Identified by a 64-bit integer (or two 32-bit integers)
  • Not tied to any table or row

Transaction-Level Advisory Locks

sql

Session-Level Advisory Locks

sql

Session-level locks survive transaction boundaries. Use them for long-running operations that span multiple transactions.

Production Pattern: Distributed Singleton Job

The most common use case: ensuring only one instance of a background job runs at a time across multiple application servers.

sql
sql

Viewing Active Advisory Locks

sql

Advisory Locks vs Application-Level Locks (Redis SETNX)

Advisory LocksRedis SETNX
Atomicity with DB transactionYes — lock and data modification in one TXNo — separate systems
Automatic cleanup on crashYes — session ends, lock releasedRequires TTL
No additional infrastructureYesNo — requires Redis
Lock acquisition overhead~microseconds~1ms network RTT
Works without Postgres connectionNoYes

For application-level coordination that also modifies database state, advisory locks win on atomicity and simplicity. For coordination between processes that do not share a Postgres connection (microservices), Redis or a distributed lock service is appropriate.


The Production Incident: ALTER TABLE Queue Cascade

Context: A team ran ALTER TABLE transactions ADD COLUMN region TEXT NOT NULL DEFAULT 'us-east' during a low-traffic window — but did not set lock_timeout.

What happened:

A background analytics query had been running for 8 minutes (no statement_timeout was set). It held ACCESS SHARE on the transactions table. The ALTER TABLE queued waiting for ACCESS EXCLUSIVE.

Over the next 30 seconds, 400 application connections tried to query transactions. Each one queued behind the ALTER TABLE. PgBouncer's connection pool filled. New application requests began failing with connection pool full. The analytics query ran for another 4 minutes.

Root cause: one 12-minute analytics query + no lock_timeout on the ALTER TABLE = 430 blocked connections.

The fix and prevention:

sql
ini

Summary

ConceptKey Takeaway
ACCESS EXCLUSIVEAcquired by ALTER TABLE, TRUNCATE, DROP. Blocks everything including SELECT. Always set lock_timeout.
Lock queue cascadingA waiting ACCESS EXCLUSIVE blocks all subsequent lock requests, even compatible ones.
FOR UPDATEExclusive row lock. Prevents concurrent modification. Released on COMMIT/ROLLBACK.
FOR NO KEY UPDATELike FOR UPDATE but doesn't block foreign key checks. Prefer when not updating key columns.
FOR SHAREShared row lock. Prevents updates but allows concurrent reads.
SKIP LOCKEDSkip locked rows. Correct pattern for work queues.
NOWAITFail immediately on lock contention. Prevents queue buildup.
Deadlock preventionAlways acquire locks in consistent order across all code paths.
Advisory locksApplication-defined cooperative locks. Ideal for distributed job deduplication.
lock_timeoutSet it on every DDL statement on a live table.

Knowledge Check

A Senior DBA executes ALTER TABLE users ADD COLUMN email TEXT; on a heavily trafficked users table. Concurrently, a long-running analytics query is executing SELECT COUNT(*) FROM users;. Shortly after the ALTER TABLE command is issued, new application requests attempting SELECT * FROM users WHERE id = 1; begin to time out, eventually leading to a connection pool exhaustion and a full outage. Which of the following best explains the root cause of this outage?


Consider a banking application where transfer_funds(sender_id, receiver_id, amount) is implemented. To prevent race conditions and ensure atomicity, the accounts table rows for both sender and receiver are locked. If two concurrent transactions attempt to transfer funds between the same two accounts (e.g., A -> B and B -> A), which of the following strategies is most effective in preventing deadlocks while maintaining data integrity?


A microservices architecture needs to ensure that a critical background job, process_daily_reports, runs on only one instance at a time across multiple application servers. This job also performs complex, multi-step database operations within a single transaction. Which locking mechanism is the most appropriate and why?

Test your knowledge with more question sets

Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.