CouchDB Replication: Engineering Bidirectional Sync Without Custom Queues
CouchDB's _replicator endpoint and MVCC model turn bidirectional sync into a built-in feature. Learn how continuous replication works, where conflicts surface, and what trade-offs to plan for.
03 Oct 2025, 19:16 UTC

The Sync Problem That Replication Solves
Mobile apps, edge devices, and multi-region services all hit the same wall: they need to keep working when the network drops, then reconcile changes once connectivity returns. Building that by hand usually means queues, timestamps, and custom merge logic that quietly drift out of sync. CouchDB takes a different route. Its HTTP-based replication protocol treats conflict as a normal state, not an error, which makes bidirectional synchronization a built-in feature rather than a bespoke integration project.
MVCC and the _replicator Endpoint
CouchDB stores every document change as a new immutable revision. This multi-version concurrency control (MVCC) model means writers never block readers, and replication can compare revision trees instead of guessing which side is newer. You drive replication through the RESTful _replicator endpoint: create a document in the _replicator database that names a source and target, and CouchDB schedules the work. Set continuous: true and the node keeps the pipeline open, watching for new changes instead of running once and exiting. True bidirectional sync simply means defining a second replication document in the opposite direction; the protocol itself is directional per job.
Worked Example: Two Nodes and a Conflict
The fastest way to see the mechanics is to run two local nodes. This example assumes CouchDB 3.x; exact startup flags depend on your installation, but the HTTP contract is stable across recent 3.x releases.
Start one instance on port 5984 and another on 5985. Create a database named mydb on each:
curl -X PUT http://localhost:5984/mydb
curl -X PUT http://localhost:5985/mydb
Add a document to the first node:
curl -X PUT http://localhost:5984/mydb/doc1 \
-H "Content-Type: application/json" \
-d '{"value":"initial"}'
Now create a replication document. Because _replicator can trigger internal HTTP requests, most hardened deployments require an administrative user or a role with replicator privileges; run this as a server admin or a user granted the _replicator write role:
curl -X PUT http://localhost:5984/_replicator/replication1 \
-H "Content-Type: application/json" \
-d '{
"source": "http://localhost:5984/mydb",
"target": "http://localhost:5985/mydb",
"continuous": true
}'
Expected check: query GET /_active_tasks on either node. You should see a replication task listing the source and target. If it is absent, the replication document was not accepted or the credentials lack permission.
To force a conflict, update doc1 on both nodes independently:
curl -X PUT http://localhost:5984/mydb/doc1 \
-H "Content-Type: application/json" \
-d '{"value":"from-5984","_rev":"<rev-from-first-write>"}'
curl -X PUT http://localhost:5985/mydb/doc1 \
-H "Content-Type: application/json" \
-d '{"value":"from-5985","_rev":"<rev-from-first-write>"}'
After replication runs, fetch the document with conflicts exposed:
curl "http://localhost:5985/mydb/doc1?conflicts=true"
The response includes a _conflicts array listing the losing revision IDs. CouchDB deterministically picks a winner, but both branches remain in the revision tree. Nothing is silently discarded.
Trade-offs: Revision History, Conflict Resolution, and Partitions
That safety has a price. Every revision stays on disk until you compact the database with POST /mydb/_compact. In write-heavy workloads, uncompacted revision history can inflate storage well beyond the live data set. Plan compaction windows accordingly.
The bigger engineering decision is conflict resolution. CouchDB's deterministic winner is stable, but it is arbitrary with respect to your business rules. If two field agents edit the same work order, the database does not know which edit reflects reality. You must supply that logic. A validate_doc_update function in a design document can reject invalid writes, but it cannot merge two valid branches. For true reconciliation, your application must read the _conflicts array, apply domain rules, and write a merged document that supersedes both branches.
Continuous replication also assumes connectivity. Network partitions cause replication to pause; prolonged outages mean divergent histories. When the link returns, CouchDB reconciles the trees and surfaces conflicts, but your monitoring should treat rising conflict counts as an operational signal, not just a data anomaly.
What to Do Next
If you are evaluating CouchDB for an offline-first or edge-sync use case, start narrow. Stand up two nodes, create a single continuous replication document, and deliberately induce a conflict. Confirm that _active_tasks shows the replication thread and that ?conflicts=true returns both branches. Then decide who owns resolution: your application code, a background worker, or a human review queue. Replication gives you the transport and the audit trail; the merge policy remains yours to define.
When you are done experimenting, remove the replication document to halt the continuous job:
curl -X DELETE "http://localhost:5984/_replicator/replication1?rev=<replicator-doc-rev>"0 replies
A thoughtful contribution can make all the difference. Be the first to share one.