Consistency & Capabilities
A vector store is honest about what it can and cannot promise, and it tells you before you ask it to do something it can't. That's the whole point of this page: two mechanisms — capabilities (static truth you can read up front) and .consistency() (a read-after-write knob that degrades safely) — and one rule that governs both: the battery never fakes a guarantee.
Capabilities: ask, don't assume
Every adapter carries a capabilities object, available as both a static and an instance property, so you can branch on it before you attempt anything:
interface VectorStoreCapabilities {
transactions: boolean // multi-op ACID — pgvector, sqlite-vec only
namedVectors: boolean // multi-vector collections — Qdrant, Weaviate
rename: boolean // renameCollection supported in place
rawSql: boolean // .whereRaw() accepts a SQL string + bindings
builtInEncoding: boolean // backend embeds text server-side — Pinecone, Weaviate
consistency: VectorConsistencyCapability
}if (vs.capabilities.transactions) {
await vs.transaction(async (tx) => { /* … */ })
} else {
// do it without a transaction, knowingly
}This is the same "ask, don't assume" contract as the rest of the ADK. A capability is a fact about the backend you can read at construction time, not a surprise you hit in production when renameCollection quietly no-ops or a "transaction" turns out to have been a no-rollback batch.
Transactions: real, or thrown — never faked
Only pgvector and sqlite-vec offer genuine multi-operation ACID transactions. Qdrant, Pinecone, Weaviate, Chroma, Milvus, the managed services, in-memory — none do. So vs.transaction(cb) follows the ADK's first rule:
// On pgvector / sqlite-vec — a real transaction: commits on resolve, rolls back on throw
await vs.transaction(async (tx) => {
await tx('docs').upsert(records)
await tx('docs').where('stale', true).delete()
})
// On Qdrant / Pinecone / anything transactions:false — throws immediately
await vs.transaction(/* … */) // → E_VECTOR_STORE_TRANSACTIONS_UNSUPPORTED, no partial workThe method exists on every store, so it's discoverable and type-checks everywhere. But on a backend that can't honour it, it throws E_VECTOR_STORE_TRANSACTIONS_UNSUPPORTED the moment you call it — before any work happens — rather than silently degrading to a batch with no rollback. A faked transaction is worse than no transaction: it's a guarantee you'll rely on and that isn't there. Gate on vs.capabilities.transactions and you'll never hit the throw by surprise.
Read-after-write: the .consistency() knob
Some backends are strongly consistent: write, and the next read sees it. Others — Pinecone, S3 Vectors, Cloudflare Vectorize, Mongo Atlas Vector Search — are eventually consistent: a write is durably acknowledged but takes a beat (sometimes several seconds) to become visible to a query. The battery models this as three modes:
| Mode | The write Promise resolves… | On timeout |
|---|---|---|
'strong' | only once the change is confirmed visible to subsequent reads | throws E_VECTOR_STORE_CONSISTENCY_TIMEOUT — never resolves unconfirmed |
'best-effort' | after polling up to a bound, whether or not visibility was confirmed | resolves anyway (the only mode that may proceed unconfirmed) |
'eventual' | on durable acknowledgement, with no visibility wait | resolves immediately after the ack |
You set it per-operation on the builder, or store-wide, with the per-op call winning:
// per-operation override (highest precedence)
await vs('docs').consistency('strong').upsert(records)
// store-wide default
const vs = await createVectorStore({ client, options: { consistency: 'strong', /* … */ } })Precedence: per-op .consistency() > store consistency option > the adapter's capabilities.consistency.default. The override is universal and that's the load-bearing part: a strongly-consistent adapter ignores it (the mode is a no-op there, default is always 'strong'), so a chain you wrote against Pinecone with .consistency('strong') keeps working verbatim when you swap the adapter to pgvector. You don't rewrite queries when you change backends — the knob is always accepted, and only does something where it needs to.
best-effort and eventual carry a write-after-write race
On an eventually-consistent backend, best-effort and eventual can miss a write-after-write hazard: if you delete a just-upserted id (especially from a concurrent writer), the delete may target a version that isn't visible yet and silently not take effect. Neither mode detects this. If you delete records you may have just written, use 'strong' — it waits until the write is visible before the delete runs.
You can read what a backend supports from its capability block:
vs.capabilities.consistency
// { configurable: false, default: 'strong', modes: ['strong'] } — pgvector, sqlite-vec, in_memory, …
// { configurable: true, default: 'strong', modes: ['strong','best-effort','eventual'] } — Pinecone, …configurable: false means the backend is already strongly consistent and the option is a no-op. configurable: true means it's eventually consistent and honours the modes listed.
Other consistency models we considered
In the interest of completeness, the consistency models that did not make the cut:
Read-before-collapse A record remains both committed and uncommitted until observed by a query. Rejected because the quantum extension is not available in the self-hosted version.
Persist-before-write A write becomes visible before it is issued, depending on the observer's frame of reference. Rejected because we could not get far enough away from the database to reproduce it.
Semantic durability The record is not stored directly. Instead, the database remembers the general idea of the record and reconstructs it when queried. Rejected because emailVerified: false kept coming back as "the user probably seems verified."
Recursive semantic persistence The database asks an LLM to summarize each record, embeds the summary, stores the embedding in a vector index, and retrieves it later so another LLM can reconstruct the original record. Rejected because after six layers of abstraction we had reinvented lossy storage with extra invoices.
Schrödinger's upsert An upsert creates and updates the record simultaneously until the result is inspected. Rejected because nobody could agree whether this counted as idempotent.
Where to go next
- The Adapter Matrix — which backend has which capabilities, and the eventual-consistency caveats per managed service.
- The Query Builder & Filters — where
.consistency()lives in the chain. - Writing an Adapter — declaring your backend's capabilities honestly.