Bookmarks are Neo4j’s way of letting you read your own writes immediately, even in a clustered environment.

Let’s see it in action. Imagine a simple application where a user creates a new "post" and then immediately wants to see that post along with any comments on it.

// User creates a post
POST /db/data/transaction
{
  "statements": [
    {
      "statement": "CREATE (p:Post {id: $postId, content: $content}) RETURN p",
      "parameters": {
        "postId": "post-123",
        "content": "My first Neo4j post!"
      }
    }
  ]
}

The response to this CREATE transaction will include a txId and a commit URL. Crucially, it also contains a bookmark in the x-neo4j-transaction-info header, like this:

x-neo4j-transaction-info: {"bookmark": "Bookmark(txId=12345, ...)"}

This bookmark is a pointer to the state of the database after your write operation completed.

Now, in a separate read operation, you want to fetch this post and its comments. Without bookmarks, if your read operation happened to go to a different replica than your write operation, you might not see the post yet because replication takes a tiny bit of time. This is the "eventual consistency" problem.

With causal consistency and bookmarks, you tell Neo4j which state you want to read from.

// User reads their post and comments, using the bookmark from the write
POST /db/data/transaction
{
  "statements": [
    {
      "statement": "MATCH (p:Post {id: $postId}) OPTIONAL MATCH (p)<-[:COMMENTED_ON]-(c:Comment) RETURN p, collect(c) AS comments",
      "parameters": {
        "postId": "post-123"
      }
    }
  ],
  "bookmark": "Bookmark(txId=12345, ...)" // The bookmark from the previous write
}

By including the bookmark in the request to the read transaction, you’re telling Neo4j: "I want to see at least the state of the database as it was after the transaction identified by this bookmark committed." Neo4j’s routing layer will ensure your read request is sent to a replica that has already processed that specific transaction, guaranteeing you see your own write immediately.

Neo4j uses a concept called "causal consistency" to achieve this. Imagine a timeline of transactions. When you perform a write, it gets a position on that timeline. A bookmark is essentially a marker on that timeline. When you issue a read with a bookmark, you’re saying "start reading from at least this point on the timeline." Neo4j’s cluster manager (the AuraDB or Neo4j Enterprise’s built-in cluster manager) keeps track of transaction ordering and ensures that reads with bookmarks are routed to a Core or Read Replica instance that has progressed to at least the state indicated by the bookmark.

The primary problem this solves is the "read-your-own-writes" anomaly in distributed systems. In many distributed databases, if you write data, and then immediately try to read it back, you might get a stale result because the write hasn’t yet propagated to the replica you’re reading from. Bookmarks, combined with causal consistency, ensure that your reads are always "aware" of your preceding writes, providing a stronger consistency guarantee than simple eventual consistency without the full overhead of strong consistency across all reads.

The x-neo4j-transaction-info header is key here. When you perform a write operation (any operation that modifies data), Neo4j returns a bookmark in this header. This bookmark is an opaque string that uniquely identifies the state of the database after that specific transaction. Your application’s responsibility is to capture this bookmark and then pass it back in subsequent read requests using the bookmark field in the transaction payload. The Neo4j driver typically handles this automatically if configured for causal consistency.

The real magic happens in how the Neo4j cluster handles this. When a read request arrives with a bookmark, the cluster’s routing layer inspects the bookmark. It then consults the transaction logs or state information maintained by the Core servers to identify which read-capable instance (a Core server or a Read Replica) has processed transactions up to and including the one identified by the bookmark. The request is then routed exclusively to such an instance. If no such instance is immediately available, the request will wait briefly until one becomes available, ensuring the causal dependency is met.

The bookmark value itself isn’t something you parse or interpret; it’s a token. It contains internal information about the transaction ID and potentially other metadata that the Neo4j cluster uses to track progress. You should treat it as an opaque string. When you perform multiple writes in sequence, you’ll receive a new bookmark after each. You can then chain these bookmarks for reads. For instance, if you do Write A, get Bookmark A, then do Write B, get Bookmark B, a read operation using Bookmark B will see both Write A and Write B. If you wanted to read only after Write A, you’d use Bookmark A.

This mechanism ensures that for any given client session using bookmarks, the sequence of reads will always see at least the effects of all preceding writes from that same session. This is crucial for user-facing applications where a user expects to see the results of their actions immediately.

The next logical step in managing distributed Neo4j is understanding how to manage multiple read consumers and ensure they all see a consistent view, which involves passing around and merging bookmarks.

Want structured learning?

Take the full Neo4j course →