What Is MVCC? Multi-Version Concurrency Control
MVCC lets readers and writers work on a database concurrently without blocking each other, by keeping multiple versions of each row instead of locking it.
MVCC, or multi-version concurrency control, is a technique databases use to let reads and writes happen concurrently without blocking each other. Instead of a reader waiting for a writer to finish (or vice versa), MVCC keeps multiple versions of each row and hands each transaction a consistent snapshot of the data as it existed at a specific point in time — so a long-running read never has to wait on, or be blocked by, a write happening at the same moment.
The problem MVCC solves
The naive way to keep concurrent transactions from corrupting each other is locking: a writer takes an exclusive lock on a row, and any reader or writer that wants that row has to wait until the lock is released. This works, but it creates contention — a slow analytical query holding a read lock can stall unrelated writes, and a long transaction can back up an entire table’s worth of traffic behind it.
MVCC sidesteps this by never making readers and writers block each other. A write doesn’t overwrite a row in place; it creates a new version of that row, tagged with the transaction that created it. A read doesn’t take a lock; it simply reads whichever version of each row was current at the moment its own transaction began. Readers see a consistent snapshot even while writers are actively creating new versions underneath them, and writers don’t have to wait for readers to finish before proceeding.
How versioning actually works
Most MVCC implementations tag each row version with transaction metadata — commonly a creation transaction ID and an expiration transaction ID (left open for the current live version). When a row is updated:
- The old version isn’t deleted immediately. It’s marked as expired by the updating transaction’s ID.
- A new version is inserted, marked as created by that same transaction ID.
- Other transactions determine which version is “theirs” to see by comparing their own snapshot — typically the set of transactions that had already committed when they started — against each row version’s creation and expiration IDs.
A transaction that started before an update sees the old version; one that started after sees the new one. Both can run at the same time without either blocking the other, because they’re each looking at a version consistent with their own snapshot rather than fighting over a single shared row.
Snapshot isolation and read consistency
The practical benefit of MVCC is a form of read consistency often called snapshot isolation: a transaction sees the database as it existed at a single point in time, for its entire duration, regardless of what other transactions commit in the meantime. This is closely related to database isolation levels — MVCC is the mechanism that makes levels like Repeatable Read and Snapshot Isolation efficient to implement without heavy locking, though it doesn’t automatically grant full serializability; some anomalies (like write skew) can still occur under snapshot isolation and require additional checks in the database engine to prevent.
MVCC is why a long-running report query doesn’t need to block writes to the tables it’s reading, and why writes don’t need to wait for that report to finish — a very different experience from lock-based systems, where a slow read can visibly stall unrelated write traffic.
The cost: old versions have to go somewhere
Keeping multiple versions of every row isn’t free. Old, no-longer-visible versions accumulate as “dead” rows until nothing could possibly need them anymore — no active transaction’s snapshot still references them. Databases that use MVCC need a garbage-collection process to reclaim that space; PostgreSQL’s is called VACUUM, and other MVCC databases have their own equivalents. Skipping or falling behind on this cleanup causes table and index bloat, which degrades performance over time even though the logical data hasn’t grown — a maintenance cost that’s specific to MVCC and doesn’t exist in pure lock-based systems.
MVCC vs traditional locking
| MVCC | Lock-based concurrency | |
|---|---|---|
| Readers block writers | No | Often, depending on isolation level |
| Writers block readers | No | Often, depending on isolation level |
| Storage overhead | Multiple row versions, needs cleanup | Single version per row |
| Read consistency | Snapshot as of transaction start | Depends on lock duration and level |
| Common in | PostgreSQL, MySQL/InnoDB, Oracle, SQLite (WAL mode) | Older or simpler engines, some in-memory stores |
Most mainstream relational databases use MVCC today precisely because read-heavy and write-heavy workloads coexisting on the same tables is the common case, not the exception, and lock contention between them scales badly. It’s a core reason engines like PostgreSQL can sustain high concurrent throughput without readers and writers constantly queuing behind each other.
MVCC and write-ahead logging
MVCC is often paired with write-ahead logging, though the two solve different problems: WAL guarantees durability and crash recovery by persisting changes to a log before applying them, while MVCC governs how concurrent transactions see and create row versions. A database can use one without the other, but combining them is common — WAL provides the durable record of every change, and MVCC governs how those changes become visible to concurrently running transactions.
The takeaway
MVCC lets database readers and writers proceed concurrently by keeping multiple versions of each row instead of relying on locks, giving every transaction a consistent snapshot of the data as it existed when that transaction began. It trades storage overhead and a need for periodic cleanup of old versions for dramatically reduced lock contention — a tradeoff that’s paid off well enough that it’s the default concurrency model in most mainstream relational databases today.
Tagged
Keep reading
The Lycoris Team · · 5 min read What Is a Stored Procedure? SQL Logic in the Database
A stored procedure is precompiled SQL saved inside the database and invoked by name, cutting network round trips and centralizing business logic.
The Lycoris Team · · 5 min read What Is a Database Trigger?
A database trigger is a procedure that runs automatically on an insert, update, or delete — enforcing rules the application layer can't guarantee.
The Lycoris Team · · 5 min read What Is Little's Law? Capacity Planning Explained
Little's Law relates the number of requests in a system, their arrival rate, and how long each one takes — a simple formula for sizing capacity.