Row Locks, Deadlocks and Queues
Learn to work with explicit row locks in PostgreSQL — observe lock waits live, reproduce and diagnose a deadlock, use NOWAIT for fail-fast behaviour, build a race-free job queue with SKIP LOCKED, and coordinate processes with advisory locks.
Concurrency bugs are the hardest to reproduce and diagnose. This course gives you a working mental model by making them happen on purpose. We build a real PostgreSQL with three tables — accounts, inventory, and a jobs queue — and run through four canonical scenarios: a row lock wait where one session blocks another, a deadlock where two sessions block each other in a cycle, a NOWAIT that fails fast instead of waiting, and a SKIP LOCKED queue where two workers pick different tasks in parallel. Along the way we read pg_stat_activity to see sessions waiting for a Lock, use pg_blocking_pids to find the culprit in one call, inspect pg_locks to see which requests are not yet granted, and use advisory locks as an application-level mutex. Every scenario is run twice: once to see what goes wrong, then with the corrective pattern applied.
What you'll build
- Open two sessions, take a FOR UPDATE lock, and watch the second session wait
- Find a blocking transaction with pg_stat_activity and pg_blocking_pids
- Read pg_locks: locktype, mode, and granted
- Reproduce a deadlock and explain why PostgreSQL terminates one session
- Prevent deadlocks with consistent lock ordering
- Use FOR UPDATE NOWAIT for immediate fail-fast on a locked row
- Build a job queue worker with FOR UPDATE SKIP LOCKED
- Take and release an advisory lock; use pg_try_advisory_lock without blocking
Contents
- A database for lock experiments
- Start it
- Connect with psql
- Two sessions side by side
- The accounts table
- The inventory table
- The jobs table
- Create the tables
- Seed accounts with deterministic balances
- Seed inventory items
- Seed the job queue
- Load the data
- Refresh statistics and confirm the server
- Session A: take an exclusive row lock
- Session B: try to write the same row
- Seeing the wait in pg_stat_activity
- Run the lock monitor while B is waiting
- Find the blocker with pg_blocking_pids
- See the blocker's pid
- Read the raw lock record in pg_locks
- A lock waiting to be granted
- Session A commits — B gets the lock and runs
- Deadlock setup: session A takes row 1
- Session B takes row 2
- Session A now wants row 2
- Session B wants row 1 — the cycle closes
- Reproduce the deadlock and read the error
- Read the DETAIL line
- Preventing deadlocks with consistent lock order
- Session A locks an inventory row
- Session B refuses to wait — NOWAIT
- The immediate error
- The NOWAIT pattern in application code
- The naive worker — a race condition waiting to happen
- Worker A: skip any locked row
- Worker B: takes the next free job
- Both workers get different jobs
- The complete worker pattern
- Advisory locks: an application-level mutex
- Session B waits for the same key
- The advisory wait shows up in pg_stat_activity
- Unlock releases the waiting session
- pg_try_advisory_lock — check without blocking
- Try to acquire a held lock — returns false
- Advisory locks in pg_locks
- Four patterns, four solutions
- Lock patterns at a glance