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.
Quick fix (TL;DR)
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.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.
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
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
| Parameter | What it batches | Sensible default |
|---|---|---|
embed_batch_size | Nodes sent per embedding call | 10-100 (provider-dependent) |
insert_batch_size | Nodes inserted per vector store call | 100-2000 (store-dependent) |
chunk_size | Node target size in tokens | 512-1024 |
num_workers | Parallel embedding requests | 2-8 (respect rate limits) |
Vector store batch limits (verify current values)
| Vector store | Recommended batch | Hard limit |
|---|---|---|
| Chroma | ~1000-5000 | ~5461 per batch |
| Pinecone | ~100-1000 | 2MB per request |
| Qdrant | ~100-500 | HTTP request size |
| Weaviate | ~100 | Configurable per class |
| pgvector | ~500-2000 | DB row / connection limits |
| FAISS (local) | No batch limit | RAM |
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
Set explicit batch sizes and use the IngestionPipeline for resumability
The IngestionPipeline is LlamaIndex's durable ingestion primitive. It applies transformations, tracks progress via a docstore, and skips already-processed documents on rerun.
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, )
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.Guard against over-sized chunks and rate-limit-safe embedding
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.
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, )
Match insert_batch_size to your vector store's hard limit
Configure insert_batch_size based on the store you use. Too large and you get 4xx or OOM; too small and ingestion is slow.
# --- 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 )
Prevention checklist
Ship these seven safeguards once and this error stops appearing in your logs.
- Use
IngestionPipelinewith adocstorefor any corpus > 1000 docs. - Set
embed_batch_sizeto 10-50 for provider-hosted embeddings. - Set
insert_batch_sizeto match your vector store's recommended batch. - Add a
TokenLimitGuardtransformation to catch oversized chunks before embedding. - Enable
show_progress=Trueand 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.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.docstore_strategy=DocstoreStrategy.UPSERTS_AND_DELETE to auto-detect changed docs and remove ones that disappear from your source.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.Related errors & hubs
Get the weekly AI-error digest
New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.