Concurrency, locking & durable writes

This section documents the current Couchbase Server 7.6.x line as published at the Couchbase Server documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. Some capabilities (Enterprise-Edition-only Analytics, auditing, encryption at rest, the Backup service and rack-zone awareness, and Capella-only App Services and Columnar) are linked, not documented in depth.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, as Couchbase iterates quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

Couchbase gives every item a CAS token that changes on each mutation, and a set of durable-write levels that hold a write until replicas confirm it. This page covers optimistic concurrency with CAS, pessimistic locking with getAndLock, and the synchronous-replication durability that replaced the old client-side PersistTo / ReplicateTo approach.

Optimistic locking with CAS

CAS (Compare-And-Swap) is an opaque 64-bit value in an item’s metadata. Every successful mutation assigns a new CAS. To update safely without holding a lock: read the item and its CAS, modify the value in the client, then replace while passing that CAS. If another writer changed the item in between, its CAS no longer matches and the server rejects the write with CasMismatchException — the client then re-reads and retries.

flowchart TD A[get id -> value, cas] --> B[modify value in client] B --> C[replace id, value, options.cas = cas] C --> D{result} D -->|success| E[done] D -->|CasMismatchException| F{attempts left?} F -->|yes| A F -->|no| G[fail: give up / surface conflict] D -->|DocumentNotFoundException| G
# Compare-and-swap retry loop
for (int attempt = 0; attempt < 10; attempt++) {
    GetResult current = collection.get("account:42");
    JsonObject acc = current.contentAsObject();
    acc.put("balance", acc.getLong("balance") - 25);
    try {
        collection.replace("account:42", acc,
            ReplaceOptions.replaceOptions().cas(current.cas()));
        return;                       # committed
    } catch (CasMismatchException e) {
        # someone else mutated account:42 -- loop, re-read, re-apply
    }
}
throw new RuntimeException("account:42 too contended, gave up after 10 attempts");
# https://docs.couchbase.com/java-sdk/current/howtos/concurrent-document-mutations.html

Use a bounded loop with a small retry budget, and apply the change to the freshly read value each time — never reuse the value from a previous attempt. A single-path change (increment a number, append to an array) is usually better expressed as a sub-document mutateIn, which is atomic server-side and needs no retry loop at all. See Concurrent Document Mutations for the full pattern.

Pessimistic locking with getAndLock

When contention is high enough that retries would thrash, take an exclusive lock instead. getAndLock returns the value and locks the item for a lock TTL (default 15 seconds, 30 seconds maximum). While locked, other writers get a CasMismatchException (or a DocumentLockedException on newer servers) and other plain get calls succeed but return a CAS of zero.

GetResult locked = collection.getAndLock("account:42", Duration.ofSeconds(10));
long lockCas = locked.cas();
JsonObject acc = locked.contentAsObject();
acc.put("balance", acc.getLong("balance") - 25);

# A mutation that passes the lock CAS both writes and releases the lock
collection.replace("account:42", acc, ReplaceOptions.replaceOptions().cas(lockCas));

# If you decide not to write, release the lock explicitly with the same CAS
# collection.unlock("account:42", lockCas);
# https://docs.couchbase.com/java-sdk/current/howtos/concurrent-document-mutations.html

The lock is released by a successful CAS mutation, by an explicit unlock, or automatically when the lock TTL expires — so a crashed client cannot wedge an item forever. Prefer pessimistic locking for short, guaranteed critical sections; prefer CAS retries when conflicts are rare.

CAS as an HTTP ETag

CAS maps directly onto HTTP conditional requests. Return the item’s CAS as the ETag of a GET response; require it back as If-Match on PUT / PATCH; translate a CasMismatchException into 412 Precondition Failed. Clients then get optimistic concurrency across the network for free.

# GET /accounts/42   -> 200, ETag: "0x16f3a9c8b4d20000"
# PUT /accounts/42   with  If-Match: "0x16f3a9c8b4d20000"
#   parse the ETag back to a long, pass it as options.cas(...)
#   on CasMismatchException -> respond 412 Precondition Failed

Durable writes (synchronous replication)

By default a KV write is acknowledged as soon as the active copy has it in memory. A durable write asks the server to hold the acknowledgement until the mutation is also on replicas and/or on disk, using synchronous replication built into the server (6.5+). Pass a DurabilityLevel:

Level Acknowledged when

majority

A majority of configured replicas (including the active) hold the mutation in memory.

majorityAndPersistToActive

A majority hold it in memory and the active node has written it to disk.

persistToMajority

A majority of nodes have written the mutation to disk.

collection.upsert("order:2026-0007", order,
    UpsertOptions.upsertOptions().durability(DurabilityLevel.MAJORITY));

# Stronger: survive an active-node disk loss immediately after ack
collection.upsert("order:2026-0007", order,
    UpsertOptions.upsertOptions().durability(DurabilityLevel.PERSIST_TO_MAJORITY));
# https://docs.couchbase.com/server/current/learn/data/durability.html
sequenceDiagram participant C as Client participant A as Active node participant R1 as Replica 1 participant R2 as Replica 2 C->>A: upsert(key, value), durability = majority A->>A: apply to active (in memory) A->>R1: replicate mutation A->>R2: replicate mutation R1-->>A: ack (in memory) Note over A,R1: majority reached (active + 1 replica) A-->>C: success R2-->>A: ack (later)

If the topology cannot possibly satisfy the level — for example majority on a bucket with zero replicas, or too few nodes online — the server fails fast with DurabilityImpossibleException rather than blocking. A durable write that is accepted but not confirmed before its timeout returns an ambiguous result and may still commit.

Relation to the older PersistTo / ReplicateTo

Older SDK documentation performs client-side durability with PersistTo and ReplicateTo options: after the write is acknowledged, the SDK polls each node with observe calls until the required number report the mutation persisted or replicated. That approach still exists for compatibility with pre-6.5 servers, but it is client-driven, several round-trips, and not atomic with the write. On Couchbase Server 7.6 use DurabilityLevel instead — the guarantee is enforced by the server as part of the mutation, in one operation. Reach for PersistTo / ReplicateTo only when talking to a server too old for synchronous replication. See Durability for the comparison and KV durability options for the SDK surface.

Contrast with MongoDB

Couchbase applies durability and concurrency per single-key operation: a DurabilityLevel is MongoDB’s write concern for one item, and a CAS-guarded replace is MongoDB’s single-document atomic update. MongoDB spells the majority guarantee w: "majority" and its on-disk variant with j: true; the concepts line up with majority and persistToMajority. See MongoDB single-document atomicity for the update-in-place guarantee and MongoDB write/read concern for the durability knobs. Multi-document ACID in Couchbase is a separate library, covered in SQL++ DML, functions & transactions.

Continue with Data modeling.