The CouchDB _changes feed doesn’t just tell you that a document changed; it tells you how it changed, enabling true real-time synchronization without constant polling.

Let’s see this in action. Imagine we have a simple todo database.

curl -X PUT http://localhost:5984/todo

Now, let’s add a document:

curl -X POST \
     -H "Content-Type: application/json" \
     -d '{"text": "Buy groceries", "completed": false}' \
     http://localhost:5984/todo

This returns something like:

{"ok":true,"id":"a1b2c3d4e5f678901234567890abcdef","rev":"1-abcdef1234567890abcdef1234567890"}

The id is the document’s unique identifier, and rev is its revision ID. Now, let’s tap into the _changes feed for this database:

curl -N http://localhost:5984/todo/_changes?feed=longpolling&since=0

The -N flag prevents chunking, and feed=longpolling (or continuous for websockets) keeps the connection open. since=0 tells CouchDB we want all changes since the beginning of time.

As soon as we run that curl command, the output will be:

{"seq":1,"id":"a1b2c3d4e5f678901234567890abcdef","changes":[{"rev":"1-abcdef1234567890abcdef1234567890"}]}
{"seq":2,"id":"another-doc-id","changes":[{"rev":"1-anotherrev"}]}
...

Each line represents a change. seq is a monotonically increasing sequence number for the feed itself. id is the document ID that changed. changes is an array containing information about the revision(s) of that document that are new.

Now, let’s update that first document. We need its current rev.

curl http://localhost:5984/todo/a1b2c3d4e5f678901234567890abcdef

This returns:

{"_id":"a1b2c3d4e5f678901234567890abcdef","_rev":"1-abcdef1234567890abcdef1234567890","text":"Buy groceries","completed":false}

We’ll change completed to true and use the rev from the previous response:

curl -X PUT \
     -H "Content-Type: application/json" \
     -d '{"_id":"a1b2c3d4e5f678901234567890abcdef", "_rev":"1-abcdef1234567890abcdef1234567890", "text":"Buy groceries", "completed":true}' \
     http://localhost:5984/todo/a1b2c3d4e5f678901234567890abcdef

This returns:

{"ok":true,"id":"a1b2c3d4e5f678901234567890abcdef","rev":"2-fedcba0987654321fedcba0987654321"}

Notice the new rev. Back in our _changes feed terminal, a new line appears:

{"seq":3,"id":"a1b2c3d4e5f678901234567890abcdef","changes":[{"rev":"2-fedcba0987654321fedcba0987654321"}]}

The _changes feed is the heart of CouchDB’s replication and synchronization mechanisms. It’s a persistent, ordered log of all document modifications. When you poll this feed, you provide a since parameter, which is the seq number from the last change you processed. CouchDB then sends you all changes that occurred after that seq. This is how clients and other CouchDB instances stay in sync. The feed can be longpolling (a single request that waits for a change, then returns, requiring a new request for the next change) or continuous (a single, long-lived connection that streams changes as they happen, often over WebSockets or Server-Sent Events for efficiency).

The _changes feed fundamentally solves the problem of efficiently knowing when and what has changed in your database without resorting to expensive full-database comparisons or timestamp-based heuristics that can easily get out of sync. It provides a reliable, ordered sequence of events that clients can consume and act upon. By using the id and rev from a change notification, you can then fetch the specific document using GET /db/doc_id to get its new state.

The _changes feed is not just for document updates. Deletions also appear in the feed. If you delete a document:

curl -X DELETE http://localhost:5984/todo/a1b2c3d4e5f678901234567890abcdef?rev=2-fedcba0987654321fedcba0987654321

The _changes feed will show:

{"seq":4,"id":"a1b2c3d4e5f678901234567890abcdef","changes":[{"rev":"3-anotherrev","deleted":true}]}

The deleted:true flag in the changes array indicates that the document was deleted. Even deleted documents retain a revision ID, which is crucial for replication to correctly propagate the deletion. You can still fetch a deleted document, but CouchDB will return a 404 Not Found status code along with the document body, which will include its _id, _rev, and a deleted:true field.

The most powerful aspect of the _changes feed, and one that’s often overlooked by newcomers, is its ability to be filtered and transformed. You can specify a view parameter and point it to a design document and view. CouchDB will then run that view and emit changes based on the view’s output, allowing you to create a custom, real-time feed of specific types of changes relevant to your application logic, rather than just every single document modification. This is done by writing a view function that uses emit() within the _changes handler.

Once you’ve mastered the _changes feed for single-node synchronization, the next logical step is understanding how CouchDB’s replication protocol leverages this feed for multi-node and distributed setups.

Want structured learning?

Take the full Couchdb course →