Documents, keys, metadata & expiration
|
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. |
A Couchbase item is the unit the data service stores and replicates. Every item is a key plus a value plus a block of metadata the server maintains for you. This page covers what goes in each part, how keys should be designed, what values may contain, how to read the metadata, and how expiration is decided.
An item is a key, a value, and metadata
The data (KV) service is a distributed hash map: it locates an item by hashing its key to one of 1024 vBuckets and routing to whichever node currently owns that vBucket. See Data Service fundamentals for the storage model and the document data model for how items map to application records.
A document key (also called the document ID) identifies an item uniquely within a collection. Rules:
-
It is a UTF-8 string, at most 250 bytes after UTF-8 encoding (multi-byte characters count for more than one byte).
-
It cannot be empty and cannot be changed — a "rename" is an insert of a new key plus a remove of the old.
-
Uniqueness is scoped to the collection, so
ordersandinvoicescollections may both hold key1001.
Key-design patterns
Predictable keys let application code compute a key instead of querying for it — the fastest possible lookup. Compose them from stable identifiers with a type prefix and a separator:
# Predictable / composite key: type prefix + natural identifiers
user:42
order:42:2026-0007
cart:session:9f3c1e7a
# The application builds the key directly, then does a single KV get:
# collection.get("order:42:2026-0007")
# https://docs.couchbase.com/server/current/learn/data/document-data-model.html
When there is no natural identifier, generate one. An atomic counter document gives dense, monotonically increasing integers; a UUID gives a collision-free key with no coordination.
# counter-generated key: increment() creates the counter at `initial` on first call,
# then returns the next value atomically across all clients
counter = collection.binary().increment(
"counter:invoice",
IncrementOptions.incrementOptions().initial(1).delta(1))
key = "invoice:" + counter.content() # -> "invoice:1", "invoice:2", ...
# UUID key: no round-trip, but keys are random (less cache-friendly, no range meaning)
key = "user:" + UUID.randomUUID() # -> "user:f47ac10b-58cc-4372-a567-0e02b2c3d479"
Counter keys are compact and sortable but serialize on one hot document; UUID keys scale writes freely but scatter across the keyspace. Prefer predictable composite keys wherever an identifier already exists.
Values: JSON and binary
Most items hold a JSON document. Storing JSON (rather than an opaque blob) is what lets the Query, Search, Analytics, and Eventing services read the same items without a separate copy.
# A JSON value, stored under key "order:42:2026-0007"
{
"type": "order",
"customerId": 42,
"status": "OPEN",
"lines": [
{ "sku": "A-1", "qty": 2, "price": 9.99 },
{ "sku": "B-7", "qty": 1, "price": 19.00 }
]
}
A value may also be binary or any non-JSON byte array (a serialized object, a small image, a Protocol
Buffers message). The trade-off: binary values are opaque to every service except KV, so they cannot be
queried, indexed by GSI, or searched, and append / prepend only work on non-JSON values. The SDK records
the content type in the item’s flags so it can deserialize correctly on read.
The hard limit is 20 MB per value (key + metadata are additional). Large values waste cache and slow replication; keep documents well under a megabyte and split or externalize anything larger. See Data Service fundamentals for the size and encoding constraints.
Metadata
The server keeps per-item metadata alongside the value:
| Field | Meaning |
|---|---|
CAS |
Compare-And-Swap token: an opaque 64-bit value that changes on every mutation. Used for optimistic locking — see Concurrency, locking & durable writes. |
expiry |
Absolute Unix time at which the item expires, or 0 for "no expiry". |
flags |
SDK-set integer describing how the value is encoded (JSON, raw binary, string). |
seqno |
Per-vBucket sequence number; every mutation on a vBucket gets the next seqno. Drives DCP, replication, and XDCR ordering. |
rev |
Revision number, incremented on each mutation; used to break ties during replica reconciliation. |
datatype |
Server flag marking the value as JSON, compressed (Snappy), or carrying XATTRs. |
Read metadata from SQL++ with the META() function, or from the SDK result object:
-- SQL++: project the metadata pseudo-document SELECT META(o).id, META(o).cas, META(o).expiration FROM `travel-sample`.inventory.hotel AS o LIMIT 3; -- https://docs.couchbase.com/server/current/n1ql/n1ql-language-reference/metafun.html
# Java SDK: the GetResult carries CAS and expiry; the value is decoded separately
GetResult r = collection.get("order:42:2026-0007",
GetOptions.getOptions().withExpiry(true));
long cas = r.cas();
Optional<Instant> exp = r.expiryTime();
JsonObject value = r.contentAsObject();
Extended Attributes (XATTRs)
Extended Attributes are metadata fields stored with the document but outside its JSON body, so they never
appear in a normal get or SELECT *. They exist so a system can attach bookkeeping to a document without
polluting the application’s data or risking a key collision.
-
System XATTRs have names starting with an underscore (for example
_syncused by Mobile/Sync Gateway,_txnused by the transactions library). They survive a document delete as part of the tombstone. -
User XATTRs have any other name and are readable and writable by applications through the sub-document API with the
xattrpath flag.
# Write a user XATTR without touching the document body (sub-document mutateIn)
collection.mutateIn("order:42:2026-0007", Arrays.asList(
MutateInSpec.upsert("audit.lastReviewedBy", "svc-audit").xattr().createPath()
));
# Read it back
LookupInResult x = collection.lookupIn("order:42:2026-0007",
Arrays.asList(LookupInSpec.get("audit.lastReviewedBy").xattr()));
See Extended
Attributes fundamentals for the virtual XATTRs ($document, $document.exptime) that expose server-computed
metadata through the same path syntax.
Expiration (TTL)
An item can be told to expire. Expiry can be set at several levels, and the effective value follows a precedence rule — see Expiration.
-
Per-item TTL — passed on
insert/upsert/replace/touch. A duration of 30 days or less is treated as relative; anything larger must be an absolute Unix timestamp (the SDKs convert for you). -
Bucket
maxTTL— a ceiling. If set, an item with no explicit TTL inherits it, and an item requesting a longer TTL is capped to it.maxTTL = 0means no ceiling. -
Collection TTL (7.6) — a per-collection default and ceiling, layered between the bucket and the item.
Precedence, from the effective TTL an item ends up with:
-
If the item sets its own TTL, that value is used — but still capped by the collection
maxTTL, then the bucketmaxTTL, if either is non-zero. -
If the item sets no TTL, it inherits the collection
maxTTL; failing that, the bucketmaxTTL; failing that, it never expires.
By default a mutation clears any existing expiry unless you ask to keep it. preserveExpiry (7.0+) on a
mutation tells the server to leave the current expiry in place:
# Update the value but keep whatever expiry the item already had
collection.replace("session:9f3c1e7a", newValue,
ReplaceOptions.replaceOptions().preserveExpiry(true));
# touch: change only the expiry, not the value
collection.touch("session:9f3c1e7a", Duration.ofMinutes(30));
# getAndTouch: read the value and extend the expiry in one operation (sliding session)
GetResult r = collection.getAndTouch("session:9f3c1e7a", Duration.ofMinutes(30));
# https://docs.couchbase.com/server/current/learn/data/expiration.html
When an item expires it is not erased immediately. The server writes a tombstone — the key and metadata with no value — so that the deletion can replicate to replicas and over XDCR. Tombstones are swept by the metadata purge interval (a per-bucket setting, three days by default); until then, expired keys still occupy a small amount of metadata space and can still be seen by DCP consumers.
SDK data structures over one document
The SDKs expose collection-like wrappers — map, list, set, queue — that are really a single JSON document (an object or an array) manipulated through atomic sub-document operations. Each method call is one server round-trip against that document; there is no separate "collection type" on the server.
# A JSON list stored as one document under key "recent:42"
List<String> recent = collection.list("recent:42", String.class);
recent.add("order:42:2026-0007"); # sub-doc arrayAppend
recent.add("order:42:2026-0008");
String first = recent.get(0); # sub-doc get by index
# A JSON map stored as one document
Map<String, Object> prefs = collection.map("prefs:42", Object.class);
prefs.put("theme", "dark"); # sub-doc upsert of a path
These are convenient for small, bounded collections (a user’s recent items, a feature-flag map). Because the whole structure is one document, it is still subject to the 20 MB value limit and every mutation contends on that one key — for large or high-contention sets, model the entries as individual documents instead. See Key-value & sub-document operations for the sub-document primitives these wrappers are built on, and Data modeling for choosing between the two.
Continue with Key-value & sub-document operations.