Skip to content

Chroma Vector Database: From Local RAG to Distributed Retrieval

Aug 22, 2026 1 min
TL;DR Chroma manages embeddings, documents, and metadata through collections; it embeds into Python locally, uses HNSW on a single node, and separates compute from storage with object storage, SSD caches, and SPANN in distributed deployments.
Table of Contents
  1. Core architecture: the collection is the operational unit
  2. Writes and queries: establish ID and embedding contracts first
  3. Indexes and metadata filtering: deployment mode changes the engine
  4. Deployment and operations: a persistent directory is not an operating model
  5. Where Chroma fits—and where it does not
  6. References

🌏 中文版

Chroma is an open-source database built for AI retrieval. It keeps vectors, source documents, metadata, and an embedding function in a collection, letting a RAG application ingest and search data with a small Python API instead of assembling a database framework first.

Its defining feature is not merely that it runs on a laptop. The official architecture spans embedded local, single-node server, and distributed modes behind a consistent API. The implementation changes with scale: single-node Chroma favors simple deployment, while distributed Chroma separates its write log, indexes, and system catalog into independent components.

This guide follows one spine: core architecture, writes and queries, indexing and metadata filtering, deployment and operations, then fit and non-fit. If you have not selected a product yet, start with the vector database comparison. This article covers what happens after Chroma makes the shortlist.

Core architecture: the collection is the operational unit

Chroma's data model has three levels: tenant, database, and collection. A collection is where indexes are built and queries run. Each record has a unique ID and embedding, plus optional document content and metadata. A database namespaces collections by application or environment; a tenant is the top-level isolation boundary.

Local mode embeds directly into a Python process. Single-node mode exposes Chroma as a service. Distributed mode splits the system into five components: Gateway, write-ahead log, Query Executor, Compactor, and System Database. According to the distributed architecture documentation, a write is persisted to the log before acknowledgment, after which the Compactor asynchronously produces vector, full-text, and metadata indexes. Query executors consult both indexes and the log to return a consistent result.

client -> Gateway -> write-ahead log -> object storage
              |              |
              |              v
              |          Compactor -> index versions
              v
        Query Executor <-> SSD cache
              |
        System Database (catalog)

This design separates compute from storage. Logs and indexes live in cloud object storage, the System Database holds catalog data, and query nodes use local SSDs as caches. The cost is that self-hosted distributed Chroma is no longer “just a Python package”; it brings multiple services and external storage into the operational boundary.

Writes and queries: establish ID and embedding contracts first

The smallest persistent example uses PersistentClient. A production ingestion pipeline should pair stable IDs with upsert, making reprocessing idempotent rather than duplicating records:

import chromadb

client = chromadb.PersistentClient(path="./chroma-data")
collection = client.get_or_create_collection(name="product-docs")

collection.upsert(
    ids=["refund-001", "shipping-001"],
    documents=[
        "Refund requests must be submitted within seven days of delivery.",
        "In-stock orders normally ship within two business days.",
    ],
    metadatas=[
        {"locale": "en", "topic": "refund", "year": 2026},
        {"locale": "en", "topic": "shipping", "year": 2026},
    ],
)

result = collection.query(
    query_texts=["How long do I have to return an order?"],
    n_results=2,
    where={"locale": "en"},
    include=["documents", "metadatas", "distances"],
)
print(result["documents"][0])

Per the data ingestion documentation, add ignores an existing ID, update modifies only an existing record, and upsert updates or creates. When only documents are supplied, the collection's embedding function creates vectors. You can instead supply embeddings, which gives the application full control over model versioning and batch inference, but query vectors must retain the same dimensionality.

Queries follow the same split. query_texts invokes the embedding function attached to the collection, while query_embeddings accepts precomputed vectors. The Query API returns distances, not a model-independent confidence percentage. Ranking thresholds therefore need calibration against your own evaluation set; one fixed distance is not a universal relevance rule.

Indexes and metadata filtering: deployment mode changes the engine

The collection configuration documentation says single-node Chroma uses HNSW for approximate nearest-neighbor search. Raising ef_search normally improves recall at the cost of query time. Raising ef_construction spends more index-build time and memory to improve recall. No setting works for every embedding model and distribution, so tune against a retrieval evaluation set and measure recall, latency, and resource use together.

Distributed Chroma and Chroma Cloud use SPANN. This is not simply the same index spread across more machines; the storage subsystem differs. Chroma's open-source notes explicitly warn that local and distributed deployments may temporarily lack complete feature or behavioral parity. A local proof of concept validates the data model and application flow, but quality and load tests must be repeated in the target deployment mode.

Metadata filtering enters get or query through where. The official grammar supports comparisons, $and, $or, $in, and $nin; arrays can be tested with $contains. where_document adds document-content constraints. In practice, encode tenant, locale, authorization scope, document type, and time as metadata so the candidate set is constrained before vector ranking instead of hiding every restriction inside query text.

result = collection.query(
    query_texts=["refund deadline"],
    n_results=5,
    where={
        "$and": [
            {"locale": "en"},
            {"year": {"$gte": 2025}},
            {"topic": {"$in": ["refund", "warranty"]}},
        ]
    },
)

Deployment and operations: a persistent directory is not an operating model

PersistentClient is convenient for development. When multiple applications need shared access, use client-server mode and mount the data directory on persistent storage. The Docker documentation also documents OpenTelemetry integration for tracing request flows and bottlenecks. At minimum, monitor query latency, error rate, write backlog, disk, and memory, and rehearse backup restoration rather than merely creating backups.

Capacity cannot be inferred from record count alone. Vector dimensionality, document size, metadata volume, index configuration, and concurrency all affect the boundary. Chroma positions single-node mode for a handful of collections and typically fewer than ten million records. That is product guidance, not a capacity guarantee. Before approaching the boundary, load-test real data and choose between a larger node, Chroma Cloud, or operating distributed Chroma yourself.

Embedding versioning is another operational contract. A collection enforces consistent dimensions, but equal dimensions do not make vectors from different models compatible. When upgrading, build a new collection, reindex the full corpus, compare both versions on the same query set, and only then switch traffic. Do not mix old and new embeddings in place.

Where Chroma fits—and where it does not

Chroma fits teams that want a short path to a RAG prototype, prefer a Python-first API, need documents and metadata beside vectors, or want an embedded workflow that can later become a service. It also fits products willing to let Chroma Cloud absorb distributed operations instead of assembling every subsystem themselves.

The non-fits are equally concrete. If the source of truth already lives in PostgreSQL and the team does not want a second data system, pgvector may be the more natural choice. Chroma should not replace a primary relational database for complex joins and transaction logic. If self-hosting distributed infrastructure is mandatory but the team lacks object-storage, SQL catalog, observability, and capacity-planning experience, a simple client API does not erase the backend's operational cost.

Chroma's central tradeoff is compelling: it makes the first semantic search extremely short while preserving a path toward a distributed architecture. The real selection question is not whether it stores vectors. It is where your team wants responsibility for embedding versions, index lifecycle, and distributed operations to live.

References