LangChain DocumentSplitter — chunks lose semantic units or exceed token limits
Chunking sits between raw documents and retrieval. Get it wrong and your RAG system either misses information (chunks too small) or wastes context (chunks too big). Neither is loud — quality just quietly degrades.
Quick fix (TL;DR)
CharacterTextSplitter splits on a single separator and often cuts mid-sentence. Fix by (a) using RecursiveCharacterTextSplitter which tries multiple separators to keep semantic units intact, (b) sizing chunks in tokens not characters (500-1000 tokens typical, 100-200 overlap), (c) using format-specific splitters for code, markdown, and HTML, and (d) considering SemanticChunker when documents have inconsistent structure.Real error messages you'll see
These are the exact strings returned by the LangChain framework and its integrations when this error occurs. Copy-paste-searching any of them should land on this page.
openai.BadRequestError: This model's maximum context length is 8192 tokens, however you requested 12481 tokens. Please reduce the length of the messages. # Split on 4000 chars but chars are ~3 tokens each; some chunks blow past limit
# Chunk 42 ends: "The refund policy applies when the customer" # Chunk 43 starts: "requests a return within 30 days of purchase." # Retrieval finds one but not the other; answer is wrong
# chunk_size=200 chars # Retrieved chunk: "See section 4.2 for details." # ← No useful information; the details are in a different chunk
Reference
Splitter classes and when to use each
| Splitter | Best for |
|---|---|
RecursiveCharacterTextSplitter | Default for prose — respects paragraph/sentence boundaries |
CharacterTextSplitter | Simple one-separator splitting; almost never what you want |
TokenTextSplitter | When you need strict token limits (embedding model constraints) |
MarkdownHeaderTextSplitter | Markdown docs with structural headers |
MarkdownTextSplitter | Markdown content splitting on prose boundaries |
HTMLHeaderTextSplitter | HTML with heading structure |
Language-specific (PythonCodeTextSplitter, etc.) | Source code |
SemanticChunker | When structure is inconsistent (uses embeddings to find boundaries) |
Chunk size and overlap starting points
| Corpus type | <code>chunk_size</code> (tokens) | <code>chunk_overlap</code> (tokens) |
|---|---|---|
| Short-form (FAQ, docs) | 200-400 | 50-100 |
| Long-form articles | 500-1000 | 100-200 |
| Technical reference | 800-1500 | 150-300 |
| Legal / contracts | 400-800 | 100-200 |
| Source code | 1000-2000 | 100-200 or by function boundary |
Root causes, ranked by frequency
Based on developer reports across LangChain forums, GitHub issues, and Discord community during 2025–2026.
- 26%CharacterTextSplitter default. Splits on
"\n\n"only; when documents lack paragraph breaks, cuts arbitrarily. - 18%Character count vs token count confusion. Setting
chunk_size=4000chars produces variable token sizes; some exceed embedding limits. - 14%Insufficient overlap. 0-50 tokens overlap means concepts spanning chunk boundaries never retrieve together.
- 10%Wrong splitter for content type. Using CharacterTextSplitter on code or Markdown; loses structure that better splitters preserve.
- 8%Chunks too small. Below 200 tokens, chunks lack self-contained context; retrieval finds fragments.
- 7%Chunks too large. Above 2000 tokens, cost balloons and retrieval precision drops.
- 7%Metadata not preserved. Splitter loses source, page number, section headers — downstream cannot cite properly.
- 10%Encoding mismatch on tokenizer. Splitting with GPT-4 tokenizer but embedding with a different model produces off-by-N drift.
Fixes — copy-paste solutions
Use RecursiveCharacterTextSplitter with token-based sizing
Combine RecursiveCharacterTextSplitter (which tries multiple separators) with a token-based length function so chunk sizes are in tokens not characters.
from langchain_text_splitters import RecursiveCharacterTextSplitter import tiktoken # Use the encoder for the embedding model you'll use downstream encoder = tiktoken.encoding_for_model("text-embedding-3-small") def token_len(text: str) -> int: return len(encoder.encode(text)) splitter = RecursiveCharacterTextSplitter( chunk_size=800, # target tokens per chunk chunk_overlap=150, # tokens of overlap between adjacent chunks length_function=token_len, # count in tokens separators=["\n\n", "\n", ". ", " ", ""], # tried in order add_start_index=True, # metadata: where in the source the chunk starts ) # Read a document with open("big_doc.txt") as f: text = f.read() chunks = splitter.split_text(text) print(f"Created {len(chunks)} chunks") for i, chunk in enumerate(chunks[:3]): print(f"\n--- chunk {i} ({token_len(chunk)} tokens) ---") print(chunk[:200] + "...") # For a Document object with metadata, use split_documents from langchain_core.documents import Document docs = [Document(page_content=text, metadata={"source": "big_doc.txt"})] chunked_docs = splitter.split_documents(docs) # Each chunk inherits source metadata; start_index added automatically
length_function parameter is the fix for the char-vs-token confusion. Always use it in production — token-based sizing is a hard requirement to stay under embedding limits.Use format-specific splitters for structured content
For non-plain-text documents, format-aware splitters preserve headers, code blocks, and semantic boundaries. Chunk quality improves dramatically.
from langchain_text_splitters import ( MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter, HTMLHeaderTextSplitter, Language, ) # --- Markdown --- md_text = """ # Introduction Some intro text. ## Section A Content of A. ## Section B Content of B. """ # First split by headers (creates chunks with header metadata) md_header_splitter = MarkdownHeaderTextSplitter( headers_to_split_on=[ ("#", "Header 1"), ("##", "Header 2"), ("###", "Header 3"), ] ) md_docs = md_header_splitter.split_text(md_text) # Then optionally sub-split large sections with the recursive splitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=100, length_function=len ) final_chunks = text_splitter.split_documents(md_docs) # --- HTML --- html_splitter = HTMLHeaderTextSplitter( headers_to_split_on=[("h1", "H1"), ("h2", "H2"), ("h3", "H3")] ) html_chunks = html_splitter.split_text(html_content) # --- Source code --- python_splitter = RecursiveCharacterTextSplitter.from_language( language=Language.PYTHON, chunk_size=1500, chunk_overlap=100, ) py_chunks = python_splitter.split_text(python_source) # Splits on class/function boundaries first, then indentation # Same pattern for JS, TS, Go, Java, C++, Rust, etc. js_splitter = RecursiveCharacterTextSplitter.from_language( language=Language.JS, chunk_size=1500, chunk_overlap=100 )
MarkdownHeaderTextSplitter preserves the header hierarchy in each chunk's metadata. Downstream, you can filter or boost by section — very useful for structured docs like API references.Use SemanticChunker when structure is inconsistent
SemanticChunker uses embeddings to find where the topic shifts and splits there. Higher cost (embedding calls at chunk time) but often better quality for messy real-world documents.
from langchain_experimental.text_splitter import SemanticChunker from langchain_openai import OpenAIEmbeddings embeddings = OpenAIEmbeddings(model="text-embedding-3-small") semantic_splitter = SemanticChunker( embeddings=embeddings, breakpoint_threshold_type="percentile", # or "standard_deviation", "interquartile" breakpoint_threshold_amount=95, # split at top 5% of shifts ) docs = semantic_splitter.create_documents([text]) print(f"Created {len(docs)} semantic chunks") # Trade-offs: # + Chunks align with topic boundaries, not arbitrary character positions # + Great for transcripts, meeting notes, unstructured reports # - Slower to build (one embedding call per sentence-pair distance) # - Cost proportional to document size # - Chunk sizes are variable; may exceed embedding limits without a cap # Combine with a hard token cap from langchain_text_splitters import RecursiveCharacterTextSplitter import tiktoken def token_len(text: str) -> int: return len(tiktoken.encoding_for_model("text-embedding-3-small").encode(text)) semantic_chunks = semantic_splitter.create_documents([text]) # Apply a hard-cap split on any semantic chunks that grew too large capper = RecursiveCharacterTextSplitter( chunk_size=1500, chunk_overlap=150, length_function=token_len ) final_docs = capper.split_documents(semantic_chunks)
RecursiveCharacterTextSplitter on a labeled test set.Prevention checklist
Ship these seven safeguards once and this error stops appearing in your logs.
- Use
RecursiveCharacterTextSplitteras the default; notCharacterTextSplitter. - Size chunks in tokens via
length_function— never by characters alone. - Include chunk_overlap of 15-25% of chunk_size to catch cross-boundary concepts.
- Use format-specific splitters (Markdown, HTML, code) when the source has structure.
- Preserve metadata (source, section, page) through splitting — needed for citations.
- A/B test chunk size against a labeled test set; do not guess.
- Match the tokenizer to the embedding model — mismatches cause silent drift.
Frequently asked questions
SemanticChunker. Or apply different splitters to different content types (short bio: keep whole; long article: split). Just track chunk size distribution — surprises hurt.ParentDocumentRetriever implements this natively.Related errors & hubs
Get the weekly AI-error digest
New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.