LlamaIndex VectorStoreIndex construction — embedding batch failures and insert_batch_size (2026) — Fix Guide (2026) | AI Error Hub
Home Providers LlamaIndex Index construction fails
LlamaIndex Indexing · VectorStoreIndex Severity: High HTTP n/a

LlamaIndex VectorStoreIndex.from_documents() — embedding batches fail mid-ingestion

The one-liner in every tutorial (<code>VectorStoreIndex.from_documents(docs)</code>) works fine on 50 documents and falls over on 50,000. Rate limits, token overflows, and out-of-memory errors surface only at real corpus sizes.

By Sana K. · Cloud AI Reliability Engineer Published 2026-07-26 Verified 2026-07-26

Quick fix (TL;DR)

Resolution: VectorStoreIndex.from_documents() embeds every node then upserts to the vector store. For large corpora it fails on (a) embedding provider rate limits, (b) single chunks exceeding the embedding model's token limit, (c) transient upstream errors that abort the whole ingestion. Fix by (a) setting insert_batch_size and embed_batch_size, (b) using IngestionPipeline with retries and a docstore for resumability, and (c) validating and truncating chunks that exceed model limits before embedding.

Real error messages you'll see

These are the exact strings returned by the LlamaIndex framework and its integrations when this error occurs. Copy-paste-searching any of them should land on this page.

OpenAI rate limit mid-ingestion
openai.RateLimitError: Error code: 429 - Rate limit reached for text-embedding-3-small in organization org-... on tokens per min (TPM): Limit 1000000, Used 998321, Requested 12048.
Chunk exceeds embedding model token limit
openai.BadRequestError: Error code: 400 - This model's maximum context length is 8192 tokens, however you requested 9847 tokens. Please reduce the length of the input.
# Node parsed at 8500 tokens; embedding model rejects
Vector store OOM on bulk insert
chromadb.errors.InvalidCollectionException: Batch size 10000 exceeds recommended 5461 for this collection. Split the batch or increase server memory.

Reference

Batch size knobs and what they control

ParameterWhat it batchesSensible default
embed_batch_sizeNodes sent per embedding call10-100 (provider-dependent)
insert_batch_sizeNodes inserted per vector store call100-2000 (store-dependent)
chunk_sizeNode target size in tokens512-1024
num_workersParallel embedding requests2-8 (respect rate limits)

Vector store batch limits (verify current values)

Vector storeRecommended batchHard limit
Chroma~1000-5000~5461 per batch
Pinecone~100-10002MB per request
Qdrant~100-500HTTP request size
Weaviate~100Configurable per class
pgvector~500-2000DB row / connection limits
FAISS (local)No batch limitRAM

Root causes, ranked by frequency

Based on developer reports across LlamaIndex forums, GitHub issues, and Discord community during 2025–2026.

  • 26%
    Rate limit hit during embedding sweep. Default embed_batch_size + no throttling exhausts OpenAI / Cohere TPM on large corpora.
  • 18%
    Single node exceeds embedding model token limit. Node parser produced an over-sized chunk (rare but not zero — code files, minified JSON).
  • 14%
    Vector store batch too large. Store-specific hard limit reached mid-insert.
  • 10%
    Transient upstream error aborts the whole ingestion. One 500 from embeddings and 40 minutes of work lost.
  • 8%
    No docstore configured — cannot resume. Restart re-embeds everything.
  • 7%
    Node count overflows in-memory embed queue. 500K+ nodes buffered in memory before insert.
  • 7%
    Cost blow-up on retry loop. Retry logic re-embeds failed batches; original embeddings paid but discarded.
  • 10%
    Concurrent workers not respecting rate limits. num_workers=8 with no coordination bursts through TPM ceiling.

Fixes — copy-paste solutions

Fix #1

Set explicit batch sizes and use the IngestionPipeline for resumability

The single most valuable change for corpora >5000 documents.

The IngestionPipeline is LlamaIndex's durable ingestion primitive. It applies transformations, tracks progress via a docstore, and skips already-processed documents on rerun.

durable_ingestion.py
from llama_index.core import VectorStoreIndex, Settings
from llama_index.core.ingestion import IngestionPipeline
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.storage.docstore import SimpleDocumentStore
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.vector_stores.chroma import ChromaVectorStore
import chromadb

# 1) Embedding — set embed_batch_size to control provider load
embed_model = OpenAIEmbedding(
    model="text-embedding-3-small",
    embed_batch_size=32,          # smaller = safer on rate limits
    num_workers=4,                # parallelism (per-provider judgment)
)

# 2) Vector store with explicit collection
chroma_client = chromadb.PersistentClient(path="./chroma_db")
collection = chroma_client.get_or_create_collection("my_docs")
vector_store = ChromaVectorStore(chroma_collection=collection)

# 3) Docstore — tracks what has already been ingested
docstore = SimpleDocumentStore.from_persist_path("./docstore.json") \
    if os.path.exists("./docstore.json") else SimpleDocumentStore()

# 4) Pipeline: node parser -> embedding -> vector store
pipeline = IngestionPipeline(
    transformations=[
        SentenceSplitter(chunk_size=512, chunk_overlap=64),
        embed_model,
    ],
    docstore=docstore,           # skips already-processed docs on rerun
    vector_store=vector_store,
)

# 5) Run — safe to interrupt and resume
nodes = pipeline.run(
    documents=your_documents,
    show_progress=True,
    num_workers=4,               # docstore parallelism
)
print(f"Processed {len(nodes)} new nodes")

# 6) Persist docstore for next run
docstore.persist("./docstore.json")

# 7) Build index from the populated vector store
index = VectorStoreIndex.from_vector_store(
    vector_store=vector_store,
    embed_model=embed_model,
)
The IngestionPipeline's docstore lookup is by content hash — the same document re-ingested is skipped, but a modified document is re-embedded. This is exactly what you want for incremental updates.
Fix #2

Guard against over-sized chunks and rate-limit-safe embedding

Pre-validate chunks and pace embedding calls.

Add a validation transformation that truncates or splits chunks exceeding the embedding model's limit. Configure the embedding model with conservative batch size and worker count for your provider's TPM.

chunk_validation.py
from typing import List
import tiktoken
from llama_index.core.schema import BaseNode, TransformComponent
from llama_index.core.node_parser import SentenceSplitter

# --- Guard: verify no chunk exceeds embedding token limit ---
class TokenLimitGuard(TransformComponent):
    """Split any node whose text exceeds the embedding model's limit."""
    max_tokens: int = 8000        # keep 192 tokens of headroom under 8192
    tokenizer_name: str = "cl100k_base"

    def __call__(self, nodes: List[BaseNode], **kwargs) -> List[BaseNode]:
        enc = tiktoken.get_encoding(self.tokenizer_name)
        out = []
        for node in nodes:
            tokens = enc.encode(node.text)
            if len(tokens) <= self.max_tokens:
                out.append(node)
                continue
            # Split into halves recursively (rare — node parser should prevent this)
            print(f"⚠ Node {node.node_id[:8]}... has {len(tokens)} tokens, splitting")
            mid = len(tokens) // 2
            first_text = enc.decode(tokens[:mid])
            second_text = enc.decode(tokens[mid:])
            # Preserve metadata
            first = node.copy()
            first.text = first_text
            first.node_id = f"{node.node_id}_a"
            second = node.copy()
            second.text = second_text
            second.node_id = f"{node.node_id}_b"
            out.extend([first, second])
        return out

# --- Configure embedding with tight rate control ---
from llama_index.embeddings.openai import OpenAIEmbedding

embed_model = OpenAIEmbedding(
    model="text-embedding-3-small",
    embed_batch_size=20,             # smaller batches on strict TPM
    max_retries=6,                    # retry on rate limits (SDK-level)
    timeout=60,
    reuse_client=True,
    additional_kwargs={"encoding_format": "float"},
)

# Wire into the pipeline
pipeline = IngestionPipeline(
    transformations=[
        SentenceSplitter(chunk_size=512, chunk_overlap=64),
        TokenLimitGuard(max_tokens=8000),   # ← guards against overflow
        embed_model,
    ],
    docstore=docstore,
    vector_store=vector_store,
)
For very large corpora, run ingestion in phases: (1) node parsing only, save intermediate nodes; (2) embedding + insert, with checkpointing. This lets you restart from the embedding phase without re-parsing.
Fix #3

Match insert_batch_size to your vector store's hard limit

Every vector store has a batch ceiling — respect it.

Configure insert_batch_size based on the store you use. Too large and you get 4xx or OOM; too small and ingestion is slow.

store_specific_batching.py
# --- Chroma: 5461 hard limit; use 2000 for safety ---
from llama_index.vector_stores.chroma import ChromaVectorStore
chroma_store = ChromaVectorStore(chroma_collection=collection)

# --- Pinecone: 2MB request cap; 100-200 for dense vectors is safe ---
from llama_index.vector_stores.pinecone import PineconeVectorStore
pinecone_store = PineconeVectorStore(
    pinecone_index=idx,
    insert_kwargs={"batch_size": 100},   # per-batch upsert size
)

# --- Qdrant: batch_size configurable in insert ---
from llama_index.vector_stores.qdrant import QdrantVectorStore
qdrant_store = QdrantVectorStore(
    client=qdrant_client,
    collection_name="my_docs",
    batch_size=64,
)

# --- pgvector via LlamaIndex ---
from llama_index.vector_stores.postgres import PGVectorStore
pg_store = PGVectorStore.from_params(
    database="mydb",
    host="localhost",
    port=5432,
    user="user",
    password="pass",
    table_name="documents",
    embed_dim=1536,
    hybrid_search=False,
    perform_setup=True,
)

# --- On the index side ---
from llama_index.core import VectorStoreIndex, StorageContext

storage_context = StorageContext.from_defaults(vector_store=chroma_store)

# insert_batch_size on the index builder controls how many nodes go in per
# vector_store.add() call
index = VectorStoreIndex(
    nodes=nodes,
    storage_context=storage_context,
    embed_model=embed_model,
    show_progress=True,
    insert_batch_size=2000,       # ← store-appropriate
)
For self-hosted stores (pgvector, Qdrant self-hosted), the effective batch size is bounded by RAM. Monitor memory during first-time ingestion of large corpora — it is easy to OOM the vector store host.

Prevention checklist

Ship these seven safeguards once and this error stops appearing in your logs.

  • Use IngestionPipeline with a docstore for any corpus > 1000 docs.
  • Set embed_batch_size to 10-50 for provider-hosted embeddings.
  • Set insert_batch_size to match your vector store's recommended batch.
  • Add a TokenLimitGuard transformation to catch oversized chunks before embedding.
  • Enable show_progress=True and log per-batch timing to catch slowdowns.
  • For rerunnable ingestion, persist the docstore between runs.
  • For huge corpora, split ingestion into phases (parse → embed) with intermediate checkpoints.

Frequently asked questions

from_documents is a convenience one-liner: parse, embed, insert, index — all in one shot with no checkpointing. IngestionPipeline is the durable primitive with docstore-backed dedup and resumability. For >5000 documents, use IngestionPipeline.
With IngestionPipeline + docstore: rerun the same code. Documents already in the docstore are skipped. Without a docstore, you cannot resume — everything re-ingests, and the vector store will have duplicates unless you configure DocstoreStrategy.UPSERTS.
No — each node is embedded independently. Batch size only affects throughput and error surface. Smaller batches = more calls but each smaller and less likely to trigger provider limits.
Yes with the docstore. Configure the pipeline with docstore_strategy=DocstoreStrategy.UPSERTS_AND_DELETE to auto-detect changed docs and remove ones that disappear from your source.
Set embed_batch_size=1 and increase num_workers to parallelize instead. Some local models (e.g. via HuggingFaceEmbedding) prefer batched calls; providers vary. Test both configurations.

Get the weekly AI-error digest

New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.