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.
# 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 |
|---|---|
|
A majority of configured replicas (including the active) hold the mutation in memory. |
|
A majority hold it in memory and the active node has written it to disk. |
|
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
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.