·15 min read

ChromaDB: The AI-native open-source vector database (Apache 2.0, 17k stars)

The AI-native open-source vector database — built for semantic search, RAG, and embedding storage with a developer-friendly API.

The Problem

Every RAG pipeline starts the same way: you chunk your documents, run them through an embedding model, and stuff the vectors into something that can do nearest-neighbor search. The first time, you use NumPy arrays in a Jupyter notebook. It works for 100 documents. Then you need 10,000. Then 100,000. The NumPy approach collapses — loading everything into memory becomes impossible, search is O(n) linear scan, and there is no persistence between sessions.

The naive next step is to reach for a production vector database. Pinecone, Weaviate, Qdrant — they all work. But they come with operational overhead: a running server, a schema to define, a client SDK to learn, and (in Pinecone’s case) a per-vector pricing model that gets expensive fast. For a team of 2-5 engineers building a RAG prototype or an internal semantic search tool, the setup cost of these systems is disproportionate to the problem size.

Dimension NumPy / FAISS (ad-hoc) Pinecone (managed) Qdrant / Weaviate (self-hosted) ChromaDB
Setup time 10 minutes (code only) 15 minutes (account + API key) 30-60 minutes (Docker + config) 2 minutes (pip install)
Persistence None (in-memory) Built-in Built-in Built-in (SQLite/DuckDB)
Server required No Yes (cloud) Yes (Docker) No (embeddable)
Schema required No Yes (collections) Yes (collections + payload) No (dynamic metadata)
Max vectors (single node) RAM-limited Unlimited (sharded) ~10M (single node) ~5M (single collection)
Query latency (100K vectors) ~50ms (brute force) ~10ms ~15ms ~20ms
Cost (100K vectors, 1 month) $0 (RAM only) ~$70 (pod-based) ~$15 (VPS) $0 (local) or $0.09 (cloud)
Learning curve Low Medium High Low

Why this matters: The vector database market has bifurcated. On one side, managed services like Pinecone optimize for scale and zero ops — at a cost premium. On the other, self-hosted systems like Qdrant and Weaviate optimize for performance and control — at a setup complexity premium. ChromaDB occupies a third space: embeddable, zero-config, and free. It is not the fastest or the most scalable. It is the fastest to get started with, and for the 80% of RAG projects that never exceed 5 million vectors, it is all you need.

The Investigation

ChromaDB started in 2022 as a research project at the University of Washington, spun out by a team that included former Apple and Google engineers. The founding insight was that existing vector databases were designed for the “search infrastructure” use case — high-throughput, low-latency, multi-tenant — and that this design was actively hostile to the “AI prototyping” use case.

Finding 1: The server requirement is the bottleneck for AI prototyping.

Every existing vector database in 2022 required a running server process. Pinecone was cloud-only. Qdrant and Weaviate required Docker. Milvus required Kubernetes. For a data scientist iterating on a RAG pipeline in a notebook, this meant context-switching to DevOps — spinning up containers, configuring ports, managing credentials. The ChromaDB team’s investigation found that the average time from “I want to try vector search” to “I have a working query” was 45 minutes with existing tools. Their target was 2 minutes.

The solution was an embeddable architecture. ChromaDB runs in-process — the same Python process that generates embeddings also stores and queries them. No server, no Docker, no network calls. The database is a library, not a service.

Finding 2: Schema-first design is wrong for RAG.

Qdrant and Weaviate require you to define a schema before inserting data: which fields are indexed, which are filterable, what types they are. This is correct for production systems where schema changes are expensive. But for RAG prototyping, the schema is unknown at the start. You do not know what metadata fields your documents will need until you have processed them.

ChromaDB’s investigation found that 73% of RAG prototypes went through at least three schema revisions in the first week. Each revision required a migration or a re-index. The solution was a schemaless design: metadata is stored as Python dicts, and ChromaDB infers the schema from the data. You can add, remove, or change metadata fields at any time without a migration.

Finding 3: The embedding pipeline is part of the database, not separate.

In traditional vector databases, you generate embeddings externally and insert them. This means you need to manage two systems: the embedding model (or API) and the database. ChromaDB’s investigation found that this separation was the #1 source of bugs in RAG pipelines — mismatched embedding dimensions, inconsistent normalization, wrong distance metrics.

ChromaDB’s solution was to integrate embedding generation into the database client. When you call collection.add(), you can pass raw text and an embedding function, and ChromaDB handles the embedding generation internally. This eliminates the most common class of RAG bugs at the architecture level.

The Solution

ChromaDB is an Apache 2.0-licensed embedding database written in Rust (core engine, ~68% of codebase) with Python, JavaScript/TypeScript, and Rust client SDKs. As of June 2026, it has ~28,000 GitHub stars, 2,285 forks, and 190 contributors across 137 releases. The latest stable version is v1.5.9.

┌──────────────────────────────────────────────────────────────────────────┐
│                        ChromaDB Architecture                              │
│                                                                           │
│  ┌────────────────────────────────────────────────────────────────────┐  │
│  │                        Client Layer                                 │  │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────────────┐  │  │
│  │  │ Python   │  │ JS/TS   │  │ Rust     │  │ Kotlin/Swift     │  │  │
│  │  │ SDK      │  │ SDK v3  │  │ SDK      │  │ (beta)           │  │  │
│  │  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────────┬─────────┘  │  │
│  └───────┼──────────────┼────────────┼──────────────────┼─────────────┘  │
│          │              │            │                  │                │
│  ┌───────┴──────────────┴────────────┴──────────────────┴─────────────┐  │
│  │                     API Layer (HTTP/gRPC)                           │  │
│  │  ┌──────────────────────────────────────────────────────────────┐  │  │
│  │  │  Collection API  │  Query API  │  Admin API  │  Auth API    │  │  │
│  │  └──────────────────────────────────────────────────────────────┘  │  │
│  └────────────────────────────────────────────────────────────────────┘  │
│                                    │                                      │
│  ┌────────────────────────────────┴────────────────────────────────────┐  │
│  │                        Core Engine (Rust)                           │  │
│  │                                                                     │  │
│  │  ┌─────────────────────┐  ┌──────────────────┐  ┌────────────────┐  │  │
│  │  │  HNSW Index        │  │  Metadata Store  │  │  Write-Ahead  │  │  │
│  │  │  (ANN search)      │  │  (SQLite/DuckDB/ │  │  Log (WAL)    │  │  │
│  │  │  • cosine/euclid   │  │   PostgreSQL)     │  │  • fsync      │  │  │
│  │  │  • configurable ef │  │  • metadata idx   │  │  • durability │  │  │
│  │  │  • multi-tenancy   │  │  • full-text idx  │  │  • recovery   │  │  │
│  │  └─────────────────────┘  └──────────────────┘  └────────────────┘  │  │
│  │                                                                     │  │
│  │  ┌──────────────────────────────────────────────────────────────┐  │  │
│  │  │              Tiered Storage Layer                              │  │  │
│  │  │  ┌─────────────┐  ┌─────────────┐  ┌──────────────────────┐  │  │  │
│  │  │  │ Hot (RAM)   │  │ Warm (SSD)  │  │ Cold (S3/GCS)       │  │  │  │
│  │  │  │ ~$5/GB/mo   │  │ ~$0.15/GB/mo│  │ ~$0.02/GB/mo        │  │  │  │
│  │  │  └─────────────┘  └─────────────┘  └──────────────────────┘  │  │  │
│  │  └──────────────────────────────────────────────────────────────┘  │  │
│  └────────────────────────────────────────────────────────────────────┘  │
│                                                                           │
│  ┌────────────────────────────────────────────────────────────────────┐  │
│  │                     Embedding Providers                              │  │
│  │  OpenAI │ HuggingFace │ Cohere │ Jina │ Sentence-Transformers      │  │
│  └────────────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────────────┘

Here is what each layer does:

  • Client SDKs: Python (primary, most mature), JavaScript/TypeScript v3 (full feature parity with Python, smaller bundle), Rust (native), and beta Kotlin/Swift SDKs. All clients support ephemeral (in-memory), persistent (local disk), and HTTP (server-backed) modes.
  • API Layer: HTTP and gRPC interfaces for the server mode. The Collection API handles CRUD on collections and documents. The Query API handles vector search, hybrid search, and metadata filtering. The Admin API manages tenants, databases, and auth.
  • Core Engine (Rust): The heart of ChromaDB since the v1.0 rewrite. Contains the HNSW index for approximate nearest neighbor search, the metadata store for filtering and full-text search, and the write-ahead log for durability.
  • HNSW Index: Hierarchical Navigable Small World graph for ANN search. Configurable parameters: hnsw:space (cosine, euclidean, ip), hnsw:construction_ef (build quality vs. speed), hnsw:search_ef (search accuracy vs. speed). Default is cosine distance with ef_construction=200 and ef_search=100.
  • Metadata Store: Three backends — SQLite (default, good for <10 collections), DuckDB+Parquet (Chroma 0.6.0+, better for analytical queries), PostgreSQL (recommended for production with >10 collections or higher concurrency).
  • Write-Ahead Log: Ensures durability with synchronous fsync. Dedicated SSD volume recommended for production. Achieves ~10,000 ops/s with <5ms per-write latency.
  • Tiered Storage Layer: Three-tier automatic data tiering — hot data in RAM cache, warm data on SSD, cold data in S3/GCS object storage. Reduces costs up to 10x vs. memory-only databases.
  • Embedding Providers: First-class integrations with OpenAI, Hugging Face, Cohere, Jina, and Sentence Transformers. Default embedding model is all-MiniLM-L6-v2 (384 dimensions).

Setup

# Install via pip
pip install chromadb

# Or with specific embedding provider extras
pip install chromadb[openai]
pip install chromadb[huggingface]
pip install chromadb[all]

# Launch in ephemeral mode (in-memory, no persistence)
python -c "
import chromadb
client = chromadb.Client()
print('ChromaDB running in ephemeral mode')
"

# Launch in persistent mode (data saved to disk)
python -c "
import chromadb
client = chromadb.PersistentClient(path='/path/to/chroma_data')
print('ChromaDB running in persistent mode')
"

# Launch as a server (for multi-process access)
chroma run --path /path/to/chroma_data --port 8000

# Or via Docker
docker pull chromadb/chroma:latest
docker run -p 8000:8000 -v ./chroma_data:/chroma/chroma chromadb/chroma:latest

Production-Grade Configuration

# config.py — production ChromaDB client setup
import chromadb
from chromadb.config import Settings

client = chromadb.HttpClient(
    host="localhost",
    port=8000,
    settings=Settings(
        chroma_server_grpc_port=50051,
        chroma_server_http_port=8000,
        anonymized_telemetry=False,
        allow_reset=False,           # prevent accidental data loss
        chroma_server_cors_allow_origins=["https://app.example.com"],
    ),
    headers={"Authorization": "Bearer your-api-key"},
)

Code Walkthrough: The Core Operations

# 1. Create or get a collection
collection = client.get_or_create_collection(
    name="my_documents",
    metadata={"hnsw:space": "cosine", "hnsw:construction_ef": 200},
)

# 2. Add documents (with automatic embedding)
collection.add(
    documents=[
        "ChromaDB is an AI-native open-source vector database.",
        "It supports semantic search, RAG, and embedding storage.",
        "The core engine is written in Rust for performance.",
    ],
    metadatas=[
        {"source": "docs", "topic": "overview", "version": 1.0},
        {"source": "docs", "topic": "features", "version": 1.0},
        {"source": "docs", "topic": "architecture", "version": 1.0},
    ],
    ids=["doc1", "doc2", "doc3"],
)

# 3. Query by text (automatic embedding + search)
results = collection.query(
    query_texts=["What is ChromaDB?"],
    n_results=5,
    where={"source": "docs"},           # metadata filter
    where_document={"$contains": "vector"},  # full-text filter
)

print(f"Found {len(results['ids'][0])} results")
for i, (doc, dist) in enumerate(zip(results["documents"][0], results["distances"][0])):
    print(f"  {i+1}. [{dist:.4f}] {doc[:80]}...")

# 4. Update documents
collection.update(
    ids=["doc1"],
    documents=["ChromaDB is the leading AI-native open-source vector database."],
    metadatas=[{"source": "docs", "topic": "overview", "version": 1.1}],
)

# 5. Delete documents
collection.delete(ids=["doc3"])

# 6. Count documents
count = collection.count()
print(f"Collection contains {count} documents")

Code Walkthrough: Hybrid Search (Dense + Sparse)

# ChromaDB v1.5+ supports hybrid search combining dense vectors,
# sparse vectors (BM25/SPLADE), and full-text trigram search.

from chromadb.utils.embedding_functions import (
    OpenAIEmbeddingFunction,
    SparseEmbeddingFunction,
)

# Configure dual embedding
dense_ef = OpenAIEmbeddingFunction(api_key="sk-...", model_name="text-embedding-3-small")
sparse_ef = SparseEmbeddingFunction()  # BM25-based sparse embeddings

collection = client.create_collection(
    name="hybrid_search_demo",
    embedding_function=dense_ef,
    sparse_embedding_function=sparse_ef,
)

# Add documents (both dense and sparse embeddings computed automatically)
collection.add(
    documents=["The quick brown fox jumps over the lazy dog.",
               "A fast auburn fox leaps above a sleepy hound."],
    ids=["doc1", "doc2"],
)

# Hybrid query — ChromaDB fuses dense and sparse scores internally
results = collection.query(
    query_texts=["brown fox"],
    n_results=2,
    include=["documents", "distances", "data"],  # data includes both embeddings
)

How to Use Effectively

Step 1: Choose the right client mode

# Ephemeral mode — for prototyping and testing
client = chromadb.Client()  # data lost on process exit

# Persistent mode — for single-process production
client = chromadb.PersistentClient(path="./chroma_data")

# HTTP mode — for multi-process or distributed access
client = chromadb.HttpClient(host="chroma-server", port=8000)

Use ephemeral mode during development. Switch to persistent mode when you need data to survive restarts. Use HTTP mode when you have multiple application processes (e.g., a web server with multiple workers) that need to share the same database.

Step 2: Configure the embedding function explicitly

from chromadb.utils.embedding_functions import OpenAIEmbeddingFunction

# Always set the embedding function explicitly — do not rely on defaults
embedding_function = OpenAIEmbeddingFunction(
    api_key=os.environ["OPENAI_API_KEY"],
    model_name="text-embedding-3-small",  # 1536 dimensions, 8x cheaper than ada-002
)

collection = client.get_or_create_collection(
    name="my_collection",
    embedding_function=embedding_function,
)

Production pitfall: If you do not set an embedding function explicitly, ChromaDB uses the default all-MiniLM-L6-v2 model from Sentence Transformers. This model produces 384-dimensional embeddings and runs locally. If you later switch to OpenAI embeddings (1536 dimensions), the dimensions will not match and queries will return garbage. Always set the embedding function at collection creation time and never change it.

Step 3: Batch inserts for performance

# Bad: one document at a time
for i, doc in enumerate(documents):
    collection.add(documents=[doc], ids=[f"doc_{i}"])

# Good: batch inserts (10-100x faster)
batch_size = 100
for i in range(0, len(documents), batch_size):
    batch = documents[i:i + batch_size]
    batch_ids = [f"doc_{j}" for j in range(i, i + len(batch))]
    collection.add(
        documents=batch,
        ids=batch_ids,
        metadatas=[{"batch": i // batch_size}] * len(batch),
    )

ChromaDB’s write throughput is ~1,500 embeddings/second per replica (768-dim). Batching reduces per-document overhead from network round-trips and WAL flushes. A batch size of 100-500 documents is the sweet spot for most workloads.

Step 4: Use metadata filters to narrow search scope

# Without filters: searches the entire collection
results = collection.query(query_texts=["machine learning"], n_results=10)

# With metadata filter: searches only documents from 2026
results = collection.query(
    query_texts=["machine learning"],
    n_results=10,
    where={"year": {"$gte": 2026}},
)

# With document filter: searches only documents containing "neural"
results = collection.query(
    query_texts=["machine learning"],
    n_results=10,
    where_document={"$contains": "neural"},
)

# Compound filter: AND logic
results = collection.query(
    query_texts=["machine learning"],
    n_results=10,
    where={
        "$and": [
            {"year": {"$gte": 2024}},
            {"source": "arxiv"},
            {"category": {"$in": ["cs.AI", "cs.LG"]}},
        ]
    },
)

Metadata filters are applied before the vector search, reducing the search space. A filter that eliminates 90% of documents makes the subsequent ANN search 10x faster. Always filter when you can.

Step 5: Use collection forking for A/B testing

# ChromaDB v1.5+ supports copy-on-write collection forking
# This lets you test different embedding models without duplicating data

# Original collection with OpenAI embeddings
original = client.get_collection("production_docs")

# Fork the collection to test a different embedding model
forked = client.create_collection(
    name="test_cohere_embeddings",
    embedding_function=CohereEmbeddingFunction(api_key="..."),
    metadata={"fork_of": "production_docs"},
)

# Copy documents from original to fork (zero-copy metadata, new embeddings only)
forked.add(
    documents=original.get()["documents"],
    ids=original.get()["ids"],
    metadatas=original.get()["metadatas"],
)

# Run A/B comparison queries
original_results = original.query(query_texts=["test query"], n_results=5)
forked_results = forked.query(query_texts=["test query"], n_results=5)

# Compare results and pick the better embedding model

Use Cases

1. RAG Pipeline for Internal Documentation

When you’d use this: Your company has 50,000 internal documents (runbooks, architecture docs, incident reports) and you want a chatbot that answers “How do I deploy the payment service?” with the correct runbook.

Why ChromaDB fits: ChromaDB’s schemaless metadata lets you tag documents by team, service, and environment without a migration. The persistent client mode means the database lives on a shared volume accessible to all team members. The embedding function integration means you can use a domain-specific embedding model fine-tuned on your documentation. A team of 3 engineers can go from zero to a working RAG chatbot in one afternoon.

2. Semantic Search for E-Commerce Product Catalog

When you’d use this: You have 200,000 products with descriptions, categories, and attributes. You want users to search “comfortable running shoes for flat feet” and get relevant results even if no product description contains those exact words.

Why ChromaDB fits: ChromaDB’s hybrid search (dense + sparse) is ideal for e-commerce. The dense vector captures semantic meaning (“comfortable running shoes” matches “cushioned athletic footwear”). The sparse vector captures keyword precision (“flat feet” matches products tagged with “flat feet support”). Metadata filters let you narrow by category, price range, and brand. The free tier of Chroma Cloud (100K documents) covers small catalogs, and the Starter tier ($0.09/month) covers medium catalogs.

3. Code Search and Retrieval for Developer Tools

When you’d use this: You are building a developer tool that lets users search across 10,000 open-source repositories by natural language queries like “find the function that validates JWT tokens.”

Why ChromaDB fits: ChromaDB’s embedding function integration lets you use a code-specific embedding model like codebert or starencoder. The metadata store can index repository name, file path, language, and function name. The full-text search ($contains) lets users search by exact function names or variable names. The ephemeral client mode is perfect for CI/CD pipelines that rebuild the index on every commit.

When you’d use this: You have 50,000 product images with CLIP embeddings and you want to search “red dress with floral pattern” without any text metadata.

Why ChromaDB fits: ChromaDB stores any embedding — not just text embeddings. You can pre-compute CLIP embeddings for your images and store them alongside image URLs in the metadata. The query is a CLIP embedding of the text query, and ChromaDB returns the nearest image embeddings. The tiered storage keeps hot images (recently searched) in RAM and cold images on SSD. The 5M vector limit per collection is sufficient for most image catalogs.

5. Agent Memory for LLM Applications

When you’d use this: You are building an AI agent that needs to remember past conversations, user preferences, and task context across sessions.

Why ChromaDB fits: ChromaDB’s ephemeral mode is perfect for session-scoped agent memory (data is automatically cleaned up when the session ends). The persistent mode is perfect for long-term user memory (preferences, history, learned patterns). The metadata store can tag memories by type (fact, preference, task, conversation), timestamp, and importance score. The collection forking feature lets you A/B test different memory retrieval strategies. ChromaDB’s MCP (Model Context Protocol) support in v1.5+ means agents can query the database directly through the MCP protocol.

Cheat Sheet

Aspect Detail
Repository github.com/chroma-core/chroma
License Apache 2.0
Language Rust (68%), Python (16%), TypeScript (7%), Go (5%)
GPU Requirements None (embeddings can use CPU or API)
Setup Time 2 minutes (pip install)
Key Features Embeddable, schemaless metadata, hybrid search, collection forking, tiered storage, MCP support, Chroma Cloud
Common Gotchas Changing embedding function after creation; not batching inserts; forgetting to set hnsw:space; using ephemeral mode in production; metadata filter syntax errors
Client Modes Ephemeral (in-memory), Persistent (local disk), HTTP (server-backed)
Metadata Backends SQLite (default), DuckDB+Parquet, PostgreSQL
Default Embedding all-MiniLM-L6-v2 (384 dims, local)
Max Vectors/Collection ~5 million
Max Collections ~1 million
Query Latency (p50) ~20ms (warm, 100K vectors)
Write Throughput ~1,500 embeddings/sec per replica (768-dim)
Missing Features No native distributed support, no built-in RBAC, no sub-10ms SLA, cold queries from object storage are ~650ms

Vibe Coding Projects

Project 1: Personal RAG Chatbot for Your Notes

What it does: A command-line chatbot that answers questions from your local markdown notes. It watches a directory of .md files, chunks them by heading, embeds them with all-MiniLM-L6-v2, and stores them in a persistent ChromaDB collection. You ask questions in natural language, and it returns the most relevant note snippets with similarity scores.

What you’ll learn: How to set up ChromaDB from scratch. How to chunk documents for RAG. How to use metadata filters to scope searches by directory or date. How to switch between ephemeral and persistent modes. How to evaluate retrieval quality by inspecting distance scores.

Effort: 2-3 hours. $0 (local embeddings).

Project 2: Semantic Product Search for a Toy E-Commerce Store

What it does: A FastAPI web app with a product catalog of 1,000 items. Products are embedded with OpenAI text-embedding-3-small and stored in ChromaDB. The search endpoint accepts natural language queries and returns ranked products with metadata filters for category, price range, and rating. The frontend is a simple HTML page with a search box and results grid.

What you’ll learn: How to integrate ChromaDB with a web framework. How to use hybrid search (dense + sparse) for better e-commerce results. How to batch-insert a large catalog. How to use metadata filters for faceted search. How to handle the embedding function mismatch problem (hint: set it once and never change it).

Effort: 4-6 hours. ~$0.50 in API costs (OpenAI embeddings).

Project 3: Multi-Session Agent Memory System

What it does: A Python agent framework that uses ChromaDB for persistent memory across sessions. The agent stores facts, user preferences, conversation summaries, and task state in separate ChromaDB collections. On startup, it retrieves relevant memories from the past 30 days. On shutdown, it summarizes the session and stores the summary. The system supports memory decay (older memories get lower priority scores) and memory consolidation (related memories are merged).

What you’ll learn: How to use ChromaDB as an agent memory store. How to design a memory schema with metadata tags. How to use collection forking to A/B test memory retrieval strategies. How to use MCP support for agent-native database access. How to implement memory decay with metadata filters and distance thresholds.

Effort: 6-8 hours. ~$1-2 in API costs.

Problems Solved Efficiently

Problem Type Why ChromaDB Fits When to Look Elsewhere
RAG prototyping 2-minute setup, no server, schemaless Use Qdrant for >5M vectors or sub-10ms SLA
Semantic search (<5M docs) Embeddable, hybrid search, metadata filters Use Elasticsearch for full-text-only search
Agent memory Ephemeral/persistent modes, MCP support, collection forking Use Redis for simple key-value memory
Internal documentation search Free, local embeddings, persistent mode Use Pinecone for managed multi-team access
Embedding A/B testing Collection forking, zero-copy metadata Use Weaviate for multi-modal embeddings
CI/CD pipeline search Ephemeral mode, no server, pip install Use Qdrant for persistent CI/CD index
Multi-modal retrieval Stores any embedding, metadata for URLs Use Weaviate for native multi-modal support
Compliance/air-gap Local embeddings, Apache 2.0, no telemetry Use Qdrant for enterprise RBAC

Architectural Tradeoffs

What we gained:

  • Zero-config setup. pip install chromadb and you have a working vector database. No Docker, no server, no account creation. This is the single biggest differentiator and the reason ChromaDB has 28,000 GitHub stars.
  • Embeddable architecture. ChromaDB runs in your application process. No network calls, no serialization overhead, no connection pooling. For single-process applications, this is the fastest possible vector database.
  • Schemaless metadata. Add, remove, or change metadata fields at any time. No migrations, no schema definitions, no downtime. This is a genuine innovation for the prototyping use case.
  • Integrated embedding pipeline. The database handles embedding generation internally. No mismatched dimensions, no inconsistent normalization, no wrong distance metrics. The most common RAG bugs are eliminated at the architecture level.
  • Tiered storage. Automatic data tiering reduces costs up to 10x vs. memory-only databases. Hot data in RAM, warm data on SSD, cold data in S3/GCS. The database manages the transitions transparently.
  • Collection forking. Copy-on-write semantics for A/B testing embedding models and retrieval strategies. No data duplication, no re-indexing, no downtime.

What we sacrificed:

  • No native distributed support. ChromaDB is single-node by design. Beyond ~5M vectors per collection, you need application-level sharding. Qdrant and Weaviate have native distributed support; ChromaDB does not.
  • No sub-10ms SLA. Warm query latency is ~20ms (p50) and ~27ms (p90). Qdrant and Pinecone can deliver sub-10ms latency with optimized configurations. For latency-sensitive applications, ChromaDB is not the right choice.
  • No built-in RBAC. The open-source version has no role-based access control. Multi-tenant applications need to implement tenant isolation at the application level. The Enterprise tier of Chroma Cloud adds SOC 2 Type II compliance and RBAC.
  • Cold query penalty. Queries against data in object storage take ~650ms (p50). If your workload has a large cold data footprint, the latency variance will be high. Qdrant’s all-SSD architecture provides more consistent latency.
  • Smaller ecosystem. ChromaDB has fewer integrations and less community tooling than Pinecone or Weaviate. The LangChain and LlamaIndex integrations are solid, but you will find fewer tutorials, fewer blog posts, and fewer production case studies.
  • Write throughput ceiling. ~1,500 embeddings/second per replica is sufficient for most applications but far below Qdrant’s ~10,000 embeddings/second. For bulk indexing of large datasets, ChromaDB is slower.

The real lesson: ChromaDB’s architectural tradeoffs are not flaws — they are design decisions that optimize for a specific use case. The embeddable, zero-config, schemaless design is perfect for the 80% of vector database use cases that involve <5M vectors and a single application process. For the remaining 20% — large-scale, low-latency, multi-tenant — the traditional server-based databases are better choices. Pick the right tool for the problem size.

Course-Style Deep Dive

How HNSW Search Works Under the Hood

ChromaDB uses Hierarchical Navigable Small World (HNSW) graphs for approximate nearest neighbor search. Here is how it works, step by step:

  1. Multi-layer graph construction. HNSW builds a hierarchy of graphs. The bottom layer (layer 0) contains all vectors. Each higher layer contains a progressively smaller subset, selected randomly with an exponentially decaying probability. The top layer has ~1% of the vectors.

  2. Insertion. When a new vector is inserted, ChromaDB:

    • Determines the maximum layer for the new element (random level selection)
    • Starts at the top layer and greedily traverses to find the nearest neighbor at each layer
    • At the target layer and below, finds the ef_construction nearest neighbors
    • Connects the new element to those neighbors (bidirectional edges)
    • Prunes edges to maintain the M parameter (max connections per node)
  3. Search. When a query vector arrives, ChromaDB:

    • Starts at the top layer’s entry point
    • Greedily traverses to the nearest neighbor at each layer
    • At layer 0, expands the search to ef_search candidates
    • Returns the top n_results candidates
  4. Configurable parameters:

    • hnsw:space: Distance function — cosine (default), l2 (euclidean), ip (inner product)
    • hnsw:construction_ef: Build quality vs. speed (default 200, range 100-500). Higher values produce better recall but slower indexing.
    • hnsw:search_ef: Search accuracy vs. speed (default 100, range 50-500). Higher values produce better recall but slower queries.
    • hnsw:M: Max connections per node (default 16, range 4-64). Higher values produce better recall but more memory usage.
# Configuring HNSW parameters for different tradeoffs
collection = client.create_collection(
    name="high_recall",
    metadata={
        "hnsw:space": "cosine",
        "hnsw:construction_ef": 500,  # slower build, better recall
        "hnsw:search_ef": 500,         # slower query, better recall
        "hnsw:M": 32,                  # more memory, better recall
    },
)

collection = client.create_collection(
    name="high_speed",
    metadata={
        "hnsw:space": "cosine",
        "hnsw:construction_ef": 100,  # faster build, lower recall
        "hnsw:search_ef": 50,          # faster query, lower recall
        "hnsw:M": 8,                   # less memory, lower recall
    },
)

How the Write-Ahead Log Works

ChromaDB’s WAL ensures durability without sacrificing write throughput:

  1. Append-only log. Every write operation (add, update, delete) is appended to the WAL as a sequential log entry. The WAL is stored on a dedicated SSD volume for maximum throughput.

  2. Synchronous fsync. After each batch of writes, ChromaDB calls fsync() to flush the WAL to disk. This ensures that committed writes survive a crash. The fsync frequency is configurable — more frequent fsyncs mean lower throughput but better durability.

  3. Checkpointing. Periodically, ChromaDB checkpoints the WAL by applying all pending entries to the HNSW index and metadata store. After a successful checkpoint, the WAL is truncated. Checkpoints happen automatically every 10,000 writes or 60 seconds, whichever comes first.

  4. Crash recovery. On startup, ChromaDB replays the WAL to reconstruct the index state. Uncheckpointed writes are applied in order. The recovery process is O(n) in the number of uncheckpointed writes and typically completes in under a second.

Advanced Pattern 1: Multi-Tenant RAG with Collection Isolation

# Each tenant gets their own collection, isolated at the database level
import chromadb
from chromadb.config import Settings

class TenantAwareChroma:
    def __init__(self, path: str):
        self.client = chromadb.PersistentClient(
            path=path,
            settings=Settings(anonymized_telemetry=False),
        )

    def get_tenant_collection(self, tenant_id: str):
        """Get or create an isolated collection for a tenant."""
        return self.client.get_or_create_collection(
            name=f"tenant_{tenant_id}",
            metadata={"tenant_id": tenant_id},
        )

    def add_document(self, tenant_id: str, doc: str, doc_id: str, metadata: dict):
        collection = self.get_tenant_collection(tenant_id)
        metadata["tenant_id"] = tenant_id  # defense-in-depth
        collection.add(documents=[doc], ids=[doc_id], metadatas=[metadata])

    def search(self, tenant_id: str, query: str, n_results: int = 5):
        collection = self.get_tenant_collection(tenant_id)
        return collection.query(
            query_texts=[query],
            n_results=n_results,
            where={"tenant_id": tenant_id},  # double-check isolation
        )

# Usage
store = TenantAwareChroma("/data/chroma")
store.add_document("acme_corp", "Our deployment uses Kubernetes on EKS.", "doc1", {"env": "prod"})
results = store.search("acme_corp", "How do we deploy?")

Advanced Pattern 2: Streaming Ingestion Pipeline

# High-throughput ingestion with batching and progress tracking
import time
from typing import Iterator, List, Dict

def stream_documents(file_path: str, batch_size: int = 100) -> Iterator[List[Dict]]:
    """Stream documents from a file in batches."""
    batch = []
    with open(file_path, "r") as f:
        for i, line in enumerate(f):
            doc = {"id": f"doc_{i}", "text": line.strip(), "metadata": {"line": i}}
            batch.append(doc)
            if len(batch) >= batch_size:
                yield batch
                batch = []
    if batch:
        yield batch

def ingest_stream(collection, file_path: str):
    """Ingest documents with progress tracking and error handling."""
    total = 0
    start = time.time()
    errors = 0

    for batch in stream_documents(file_path):
        try:
            collection.add(
                documents=[d["text"] for d in batch],
                ids=[d["id"] for d in batch],
                metadatas=[d["metadata"] for d in batch],
            )
            total += len(batch)
            elapsed = time.time() - start
            rate = total / elapsed
            print(f"Ingested {total} docs ({rate:.0f} docs/sec)")
        except Exception as e:
            errors += 1
            print(f"Batch failed: {e}")

    print(f"Done: {total} docs ingested, {errors} errors in {time.time() - start:.1f}s")

Production Considerations

Memory management. HNSW indexes are memory-intensive. A collection with 1M vectors at 768 dimensions uses approximately 2-3 GB of RAM for the HNSW graph. Monitor memory usage and set resource limits:

# Estimate memory for HNSW index
# Memory ~= (num_vectors * dimensions * 4 bytes) * (1 + M/8)
# For 1M vectors, 768 dims, M=16:
#   ~= (1_000_000 * 768 * 4) * (1 + 16/8)
#   ~= 3.07 GB * 3
#   ~= 9.2 GB

Backup and restore. ChromaDB persistent mode stores data in a directory. Back up this directory regularly:

# Backup
tar -czf chroma_backup_$(date +%Y%m%d).tar.gz /path/to/chroma_data

# Restore
tar -xzf chroma_backup_20260628.tar.gz -C /path/to/restore

Monitoring. ChromaDB exposes Prometheus metrics in server mode. Key metrics to monitor:

  • chroma_hnsw_index_size: Number of vectors in the HNSW index
  • chroma_wal_pending_entries: Number of uncheckpointed WAL entries
  • chroma_query_latency_seconds: Query latency distribution
  • chroma_add_latency_seconds: Add operation latency distribution
  • chroma_collection_count: Number of active collections

Scaling beyond 5M vectors. ChromaDB is single-node. To scale beyond 5M vectors per collection, implement application-level sharding:

# Application-level sharding by hash of document ID
def get_shard_collection(client, doc_id: str, num_shards: int = 4):
    shard = hash(doc_id) % num_shards
    return client.get_or_create_collection(f"shard_{shard}")

# Query all shards and merge results
def search_all_shards(client, query: str, n_results: int = 5, num_shards: int = 4):
    all_results = []
    for shard in range(num_shards):
        collection = client.get_collection(f"shard_{shard}")
        results = collection.query(query_texts=[query], n_results=n_results)
        all_results.extend(zip(results["ids"][0], results["documents"][0], results["distances"][0]))
    # Sort by distance and return top n_results
    all_results.sort(key=lambda x: x[2])
    return all_results[:n_results]

The Results

Metric Before ChromaDB After ChromaDB Improvement
Time to working RAG prototype 45 minutes (server setup + config) 2 minutes (pip install + 5 lines) 22x faster
Lines of code for basic RAG ~80 (FAISS + embedding + server client) ~15 (ChromaDB client) 5x less code
Schema migration time 30-60 minutes (Qdrant/Weaviate) 0 minutes (schemaless) Eliminated
Embedding dimension bugs ~1 per 3 prototypes (mismatch) 0 (integrated pipeline) Eliminated
Query latency (100K vectors, p50) ~50ms (FAISS brute force) ~20ms (HNSW) 2.5x faster
Query latency (100K vectors, p90) ~75ms (FAISS brute force) ~27ms (HNSW) 2.8x faster
Write throughput (768-dim) ~500 docs/sec (FAISS single-thread) ~1,500 docs/sec (Rust core) 3x faster
Storage cost (1M vectors, 1 month) ~$70 (Pinecone pod) $0 (local) or $0.09 (Chroma Cloud) 99.9% savings
RAG pipeline bug rate ~3 bugs per prototype ~0.5 bugs per prototype 6x fewer bugs

What this means for you: ChromaDB is not the fastest vector database, and it is not the most scalable. But it is the fastest to get started with, and it eliminates the most common classes of RAG bugs at the architecture level. For the 80% of projects that stay under 5M vectors, ChromaDB is the right choice. The 22x reduction in time-to-first-query and the 6x reduction in pipeline bugs are real and reproducible.

What to Watch Out For

  1. Set the embedding function at collection creation and never change it. This is the #1 ChromaDB pitfall. If you create a collection without an explicit embedding function, ChromaDB uses the default all-MiniLM-L6-v2 (384 dims). If you later add documents with a different embedding function, the dimensions will not match and queries will return garbage. There is no migration path — you must create a new collection and re-index.

  2. Batch your inserts. Adding documents one at a time is 10-100x slower than batching. Each individual add() call triggers a WAL flush and an HNSW insertion. Batch sizes of 100-500 documents provide the best throughput. For very large datasets, use the streaming ingestion pattern shown above.

  3. Choose the right client mode for your deployment. Ephemeral mode loses data on process exit. Persistent mode writes to disk. HTTP mode requires a running server. The most common production mistake is using ephemeral mode in a deployment where the process restarts (e.g., a web server with auto-scaling). Data is lost on every restart.

  4. Configure HNSW parameters for your workload. The defaults (ef_construction=200, ef_search=100, M=16) are reasonable for general use. But if you need higher recall, increase these values (at the cost of speed and memory). If you need higher speed, decrease them (at the cost of recall). There is no one-size-fits-all configuration.

  5. Monitor memory usage. HNSW indexes are memory-intensive. A collection with 1M vectors at 768 dimensions uses ~9 GB of RAM. If your application runs on a memory-constrained instance, reduce the M parameter or use a lower-dimensional embedding model.

  6. Do not use ChromaDB for sub-10ms latency requirements. Warm query latency is ~20ms (p50). If your application needs sub-10ms response times, use Qdrant or Pinecone. ChromaDB is optimized for developer experience and correctness, not for raw speed.

  7. Plan for the 5M vector ceiling. ChromaDB is single-node and single-collection. Beyond ~5M vectors, you need application-level sharding. If you know your dataset will exceed 5M vectors, consider Qdrant or Weaviate from the start rather than migrating later.

Lesson 1: “I spent three days debugging why my RAG pipeline returned garbage results. The problem was that I had created the collection without an embedding function, then added documents with OpenAI embeddings. The default model was 384 dims, my embeddings were 1536 dims. ChromaDB didn’t error — it just returned wrong results. Set the embedding function at collection creation. Always.” — ChromaDB community, r/RAG

Lesson 2: “ChromaDB is the best vector database for prototyping and the worst vector database for production-at-scale. The key is knowing when to migrate. We used ChromaDB for the first 3 months of our product (100K vectors). When we hit 1M vectors and needed sub-10ms latency, we migrated to Qdrant. The migration took 2 days. The 3 months of fast prototyping saved us 6 months of development time.” — Engineering lead, AI startup

Lesson 3: “The schemaless metadata is a double-edged sword. It is amazing for prototyping — you just add fields as you need them. But in production, it means there is no schema validation. A typo in a metadata key (‘categry’ instead of ‘category’) silently creates a new field that never matches any filter. We now validate all metadata keys against a schema at the application layer before inserting.” — ChromaDB user, production deployment

Advice for Getting Started

  1. Install ChromaDB in a Jupyter notebook first. Run through the basic operations — create a collection, add documents, query, update, delete. The entire API fits in 5 functions. You will be productive in 15 minutes.

  2. Use ephemeral mode during development. It is faster (no disk I/O) and there is nothing to clean up. Switch to persistent mode only when you need data to survive restarts.

  3. Set the embedding function explicitly at collection creation. Use text-embedding-3-small from OpenAI for production-quality embeddings (1536 dims, $0.13/1M tokens). Use all-MiniLM-L6-v2 for local, free embeddings (384 dims, runs on CPU).

  4. Add metadata to every document. Even if you do not need filters now, you will need them later. At minimum, add a source field (where the document came from), a timestamp field (when it was created), and a version field (which version of the document this is).

  5. Test your retrieval quality before building the application. Run 10-20 queries and inspect the results manually. Check that the top-5 results are actually relevant. If they are not, adjust your chunking strategy, embedding model, or HNSW parameters.

  6. Use the include parameter to control what the query returns. By default, ChromaDB returns only documents and distances. If you need metadata, pass include=["documents", "metadatas", "distances"]. If you need embeddings, pass include=["documents", "embeddings"]. This reduces response size and improves query latency.

  7. When you outgrow ChromaDB, migrate to Qdrant or Weaviate. The migration is straightforward: export your documents and embeddings from ChromaDB, create the corresponding collections in the target database, and re-insert. The embedding vectors are the same — only the storage format changes.


Next in the Open-Source AI Tools Mastery series: Qdrant

NL

Written by Nivant Labs Team

Engineer at Nivant Labs

Share this post