A Deep Dive into MVCC in PostgreSQL: How Concurrency Really Works
Concurrency is one of the hardest problems in database systems — not because it’s complex in theory, but because it becomes unpredictable…
A Deep Dive into MVCC in PostgreSQL: How Concurrency Really Works

A Deep Dive into MVCC in PostgreSQL
Concurrency is one of the hardest problems in database systems — not because it’s complex in theory, but because it becomes unpredictable under real-world load.
As systems scale, multiple users attempt to read and write the same data simultaneously. Without a proper concurrency model, this leads to inconsistency, blocking, and performance collapse.
PostgreSQL addresses this challenge using Multi-Version Concurrency Control (MVCC) — a design that prioritizes consistency and performance without relying on heavy locking.
This article explores MVCC from the ground up: why it exists, how it works internally, and what trade-offs it introduces in production systems.
The Problem with Traditional Locking
Before MVCC, most relational databases relied on locking-based concurrency control, particularly Two-Phase Locking (2PL).
In this model:
- A transaction reading data acquires a shared lock
- A transaction modifying data requires an exclusive lock
This creates a fundamental limitation:
READ → blocks WRITE
WRITE → blocks READ and WRITE
While this guarantees consistency, it introduces:
- Lock contention under high concurrency
- Increased latency due to waiting
- Reduced throughput
In high-traffic systems, this becomes a bottleneck.
The MVCC Principle
PostgreSQL takes a different approach:
Reads don’t block writes, and writes don’t block reads.
Instead of locking rows, PostgreSQL maintains multiple versions of each row.
This allows:
- Readers to access a consistent snapshot
- Writers to create new versions without blocking others
The Core Idea: Versioned Rows
Rather than modifying data in place, PostgreSQL treats every change as a new version.
Conceptually:
Time ─────────────▶
Version 1 → visible to older transactions
Version 2 → visible after commit
Each transaction sees a snapshot of the database at a specific point in time.
This is the foundation of MVCC.
Tuple Structure and System Columns
Every row (tuple) in PostgreSQL contains hidden metadata:
| user_data | xmin | xmax |
xmin: Transaction ID that created the tuplexmax: Transaction ID that invalidated the tuple (0 if still valid)
These fields are not exposed in normal queries, but they drive all visibility decisions.
How Operations Work Internally
INSERT
A new tuple is created:
xmin = current TXID
xmax = 0
The row becomes visible after the transaction commits.
DELETE
The row is not physically removed:
xmax = current TXID
This marks the row as no longer valid for future transactions.
UPDATE (Important)
PostgreSQL does not support in-place updates.
Instead:
OLD ROW → xmax = current TXID
NEW ROW → xmin = current TXID
This means:
Every UPDATE creates a new version of the row.
This design is essential for enabling non-blocking reads.
Snapshots: The Heart of MVCC
Every transaction operates on a snapshot.
A snapshot captures the state of transactions at a specific moment:
xmin → oldest active transaction
xmax → next available transaction ID
xip_list → active transactions
This snapshot allows PostgreSQL to determine which rows are visible.
Visibility Rules (How PostgreSQL Thinks)
When executing a query, PostgreSQL evaluates each tuple using the snapshot.
Step 1: Check Creation (xmin)
If xmin ≥ snapshot_xmax → invisible (future data)
If xmin ∈ xip_list → invisible (not committed)
Else → potentially visible
Step 2: Check Deletion (xmax)
If xmax = 0 → visible
If xmax ≥ snapshot_xmax → visible (deleted in future)
If xmax ∈ xip_list → visible (deletion not committed)
Else → invisible (dead tuple)
Dead Tuples: The Cost of Versioning
Because PostgreSQL preserves old versions, outdated rows accumulate as:
Dead Tuples
These are:
- Not visible to any transaction
- Still stored on disk
- Still scanned during queries
Example
Live rows: 1M
Dead tuples: 5M
Total scan: 6M rows
This leads to:
- Increased I/O
- Higher CPU usage
- Slower queries
This phenomenon is known as table bloat.
VACUUM: Managing Data Lifecycle
To handle dead tuples, PostgreSQL uses VACUUM.
VACUUM acts as a garbage collector for the database.
Responsibilities
Space Reclamation
Dead tuples → marked reusable (Free Space Map)
Note: Disk size does not shrink.
Transaction ID Wraparound Prevention
Transaction IDs are 32-bit:
Max ≈ 2 billion transactions
To prevent wraparound issues, VACUUM freezes old tuples:
xmin → FrozenXID
Visibility Map Updates
VACUUM tracks pages that contain only visible rows, enabling:
- Faster index-only scans
- Better query planning
AutoVacuum: Continuous Maintenance
PostgreSQL runs VACUUM automatically using AutoVacuum.
Trigger ≈ 20% of table modified
However, defaults are not always optimal for large tables.
Example
Table size: 100M rows
Threshold: 20M changes
This delay can cause severe bloat.
Trade-offs of MVCC
MVCC improves concurrency, but introduces complexity:
Storage Overhead
Each tuple includes metadata (~23 bytes).
Write Amplification
Updates generate new rows and may update indexes.
Maintenance Requirement
Dead tuples require continuous cleanup via VACUUM.
Practical Considerations
Long-Running Transactions
A long-lived transaction holds an old snapshot:
VACUUM cannot remove old versions
Result:
- Table bloat
- Performance degradation
HOT Updates
If an update does not affect indexed columns:
Update stays within same page
Indexes untouched
This significantly improves performance.
Tuning AutoVacuum
For large tables:
Reduce autovacuum_vacuum_scale_factor
Increase frequency
This prevents large cleanup spikes.
Conclusion
MVCC is one of PostgreSQL’s most powerful features.
It enables:
- High concurrency without blocking
- Consistent reads across transactions
- Scalable performance under load
However, it shifts complexity into:
- Storage management
- Visibility rules
- Background maintenance
Understanding MVCC is essential for anyone working with PostgreSQL in production.
PostgreSQL does not hide complexity — it exposes it. And mastering MVCC is the key to mastering PostgreSQL.
메타데이터
- post_id
- b343a005ccc6
- slug
- a-deep-dive-into-mvcc-in-postgresql-how-concurrency-really-works-b343a005ccc6
- url
- https://medium.com/@navidbarsalari/a-deep-dive-into-mvcc-in-postgresql-how-concurrency-really-works-b343a005ccc6
- canonical_url
- https://medium.com/@navidbarsalari/a-deep-dive-into-mvcc-in-postgresql-how-concurrency-really-works-b343a005ccc6
- author_url
- https://medium.com/@navidbarsalari
- status
- ok
- fetched_at
- 2026-06-20 20:29:01