MVCC: The "Just Ship More Versions" Approach to Concurrency

Traditional locking in databases is like having one bathroom at a conference. MVCC (Multi-Version Concurrency Control) said "what if we just built more bathrooms?" but for data.

Instead of making transactions wait in line, MVCC creates a new version of data with each write. Every transaction gets its own snapshot—its own view of reality—and reads don't block writes. Writes don't unnecessarily block reads. Everyone's shipping.

How It Actually Works

In MySQL's InnoDB (the default storage engine), every row has hidden metadata:

  • trx_id: Which transaction last touched this row
  • roll_pointer: Points to the undo log (your version history)

When your transaction reads data, InnoDB checks: "Should you see this version?" The rules are straightforward:

  1. If the row was modified by a committed transaction that started before yours → visible
  2. If it was modified by a transaction that started after yours → not visible
  3. If you modified it yourself → always visible

The undo log stores historical versions. Snapshot reads pull from this log without locking anything. It's Git for your database rows.

What This Means for Your Isolation Levels

  • REPEATABLE READ (InnoDB's default): Your transaction sees a consistent snapshot from start to finish. No phantom reads.
  • READ COMMITTED: Each query sees the latest committed data. More current, less consistent.
  • SERIALIZABLE: Reads acquire locks. MVCC takes a backseat.

Most apps run on REPEATABLE READ and never think about it. If you're doing analytics or reports, you probably want READ COMMITTED to see fresher data.

The Philosophy: Design Away From Locks

MVCC isn't just a database trick. It's a mindset:

1. Locks are expensive—avoid them when possible
Reads and writes don't need to block each other. Version your data and let transactions decide what they see.

2. Snapshots > Real-time sync
Not everything needs to be perfectly current. Give each operation its own consistent view.

3. Version everything
Soft deletes, optimistic locking, event sourcing—all variations on "keep the old version around."

4. Async by default
Non-critical writes? Queue them. Don't make users wait.

How This Applies to Your Stack

If you're building a SaaS app with Next.js and Postgres:

Read-write separation: Use a read replica for analytics dashboards. Your main DB handles writes, replicas handle reads. No contention.

Optimistic concurrency: Add a version column. When updating, check WHERE version = old_version. If it fails, someone else updated it. Handle the conflict.

Caching with versions: Store {data, version} in Redis. On write, bump the version. Readers know when their cache is stale.

Async processing: Use a job queue (Inngest, BullMQ) for emails, webhooks, analytics. Don't block the request.

Snapshot queries: Running a big report? Query a read replica or a snapshot. Don't lock your production DB.

The Pattern

High concurrency isn't about adding more locks. It's about:

  1. Versioning your data
  2. Giving operations their own snapshots
  3. Using async processing when you can
  4. Designing away from blocking

MVCC is how Postgres and MySQL handle millions of concurrent transactions. The same principles work at the application level.

You don't need to implement your own MVCC (please don't). But you can steal the ideas: version your entities, embrace snapshots, and stop making everything wait for locks.

Related Concepts

  • Optimistic locking: MVCC's application-level cousin
  • Event sourcing: MVCC but you keep all the versions
  • CRDTs: MVCC for distributed systems without a central database
  • Postgres MVCC: Similar to InnoDB but with VACUUM instead of undo logs

MVCC shipped decades ago and it's still how we build concurrent systems. Maybe there's something to this "just version everything" approach.

T
Written by TheVibeish Editorial