---
url: 'https://adk.nht.io/batteries/vector/consistency.md'
description: >-
  capabilities as static truth, the .consistency(mode) chain method, and why
  transactions are capability-gated and never faked.
---

# Consistency & Capabilities

## LLM summary — Consistency & Capabilities

* Every adapter exposes a static + instance `capabilities: VectorStoreCapabilities` — fixed truth about the backend, readable BEFORE attempting an operation: `{ transactions, namedVectors, rename, rawSql, builtInEncoding, consistency }`.
* `consistency: VectorConsistencyCapability { configurable, default, modes }`. `configurable:false` → backend is already strongly consistent (in\_memory, orama, pgvector, sqlite-vec, and the SQL/server backends that refresh-on-write); the `.consistency()` option is a no-op and `default` is `'strong'`. `configurable:true` → an eventually-consistent backend (Pinecone, S3 Vectors, Cloudflare Vectorize, Mongo Atlas) that honours the option.
* `VectorConsistency = 'strong' | 'best-effort' | 'eventual'`. strong = the write Promise does not resolve until the change is confirmed visible; on timeout it THROWS `E_VECTOR_STORE_CONSISTENCY_TIMEOUT`, never resolves unconfirmed. best-effort = poll up to a bound, then resolve whether or not visibility was confirmed (the only mode that may proceed unconfirmed). eventual = resolve on durable ack, no visibility wait.
* Precedence: per-operation `.consistency(mode)` on the builder > store-level `consistency` option > adapter's `capabilities.consistency.default`. The per-op override is universal — strongly-consistent adapters ignore it, so a chain written for an eventually-consistent backend keeps working verbatim when the adapter is swapped.
* `vs.transaction(cb)` exists on every store (discoverable + type-checkable) but THROWS `E_VECTOR_STORE_TRANSACTIONS_UNSUPPORTED` on a backend whose `capabilities.transactions === false` — no partial work, no faked rollback. Only pgvector and sqlite-vec are `transactions:true`. Callers gate on `vs.capabilities.transactions`.
* best-effort and eventual inherit a write-after-write race on eventually-consistent backends: deleting a just-upserted id from a concurrent writer may not take effect, undetected. Use strong if you delete records you may have just written.
* The ethos: never fake a guarantee. Capabilities let a consumer branch on what's real rather than discover a backend's limits at runtime.

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:

```typescript
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
}
```

```typescript
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:

```typescript
// 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 work
```

The 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:

```typescript
// 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.

::: warning 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:

```typescript
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](./adapters) — which backend has which capabilities, and the eventual-consistency caveats per managed service.
* [The Query Builder & Filters](./query-builder) — where `.consistency()` lives in the chain.
* [Writing an Adapter](./custom-adapter) — declaring your backend's capabilities honestly.
