Handling Concurrent Updates with CouchDB MVCC
Learn how CouchDB uses Multiversion Concurrency Control (MVCC) to handle concurrent updates without locking, and how to resolve revision conflicts in your application.
17 Mar 2026, 20:54 UTC

The Problem: Avoiding the "Lost Update"
In many databases, updating a record requires a lock to prevent two users from overwriting each other's changes. However, in distributed systems, locking creates bottlenecks and availability risks. CouchDB solves this using Multiversion Concurrency Control (MVCC). Instead of locking, it tracks versions. The challenge for developers is that MVCC doesn't prevent conflicts; it detects them, meaning your application must be prepared to resolve divergent versions of the same document.
How MVCC Tracks Document History
Every document in CouchDB has a _rev (revision) ID. This ID is a string consisting of a monotonically increasing integer and a hash (e.g., 2-a1b2c3d4). The integer represents the generation of the document.
Internally, CouchDB maintains a revision tree. When you update a document, CouchDB doesn't overwrite the old data; it appends a new revision to the tree. This allows readers to continue accessing the previous version while a writer is computing the next one, eliminating read-write contention.
Inspecting the Revision Tree
You can inspect the history of a document by adding revs=true to your request. This reveals the _revisions array, which shows the path the document took to reach its current state.
# Run this against your CouchDB instance curl http://admin:password@localhost:5984/mydb/docid?revs=true
Managing Concurrent Writes and Conflicts
A conflict occurs when two clients attempt to update the same document version simultaneously. Because CouchDB requires the current _rev to be sent with a PUT request, it can tell if a client is updating an outdated version.
Example: Generating a Conflict
Imagine a document user_profile at revision 1-xxx. Two clients, A and B, both fetch this version. Client A saves first, moving the document to 2-aaa. Client B then attempts to save using 1-xxx.
# Client A updates successfully
curl -X PUT http://admin:password@localhost:5984/mydb/user_profile \
-H "Content-Type: application/json" \
-d '{"_rev":"1-xxx", "city":"New York"}'
# Client B attempts update with the same base revision
curl -X PUT http://admin:password@localhost:5984/mydb/user_profile \
-H "Content-Type: application/json" \
-d '{"_rev":"1-xxx", "city":"London"}'
Client B will receive a 409 Conflict error. In a single-node setup, the second write is rejected. However, during replication between two nodes, both writes may be accepted, creating two "leaf" revisions. CouchDB will deterministically pick one as the winner, but the other remains in the tree as a conflict.
Resolving Conflicts in the Application
CouchDB does not automatically merge fields because it cannot know your business logic (e.g., whether the latest timestamp wins or if values should be summed). To resolve a conflict, you must:
- Fetch the document with
conflicts=trueto identify the diverging revision IDs. - Retrieve the actual content of the conflicting revisions using
open_rev=all. - Merge the data in your application code.
- Save the merged document and delete the conflicting revision IDs.
Conflict Resolution Configuration
To resolve a conflict, you must explicitly delete the losing revision. If you only save a new winning version, the old conflict remains in the revision tree, consuming space.
# 1. Identify the conflict curl http://admin:password@localhost:5984/mydb/user_profile?conflicts=true # 2. Delete the losing revision (e.g., 2-bbb) curl -X DELETE http://admin:password@localhost:5984/mydb/user_profile?rev=2-bbb
Limitations and Maintenance
MVCC's primary trade-off is storage. Because CouchDB keeps old revisions to support replication and conflict detection, the database file can grow significantly even if the number of active documents remains constant.
To mitigate this, you must use compaction. Compaction removes old, unreachable revisions from the disk. You can trigger this via the _compact endpoint or by enabling automatic compaction in the configuration. Without regular compaction, replication performance may degrade as the revision tree grows unnecessarily large.
Verification
To verify your conflict resolution worked, run a GET request with conflicts=true. If the _conflicts array is missing or empty, the document has been successfully linearized.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.