LangChain document splitter — chunk_size, overlap, and semantic loss (2026) — Fix Guide (2026) | AI Error Hub
Home Providers LangChain Chunk sizing
LangChain Retrieval · Document Splitting Severity: Medium HTTP n/a

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.

By Ahmed R. · Senior AI Infrastructure Engineer Published 2026-07-26 Verified 2026-07-26

Quick fix (TL;DR)

Resolution: The default 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.

Chunks exceed embedding token limit
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
Semantic units broken across chunks
# 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
Chunks too small — no useful context
# 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

SplitterBest for
RecursiveCharacterTextSplitterDefault for prose — respects paragraph/sentence boundaries
CharacterTextSplitterSimple one-separator splitting; almost never what you want
TokenTextSplitterWhen you need strict token limits (embedding model constraints)
MarkdownHeaderTextSplitterMarkdown docs with structural headers
MarkdownTextSplitterMarkdown content splitting on prose boundaries
HTMLHeaderTextSplitterHTML with heading structure
Language-specific (PythonCodeTextSplitter, etc.)Source code
SemanticChunkerWhen 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-40050-100
Long-form articles500-1000100-200
Technical reference800-1500150-300
Legal / contracts400-800100-200
Source code1000-2000100-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=4000 chars 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

Fix #1

Use RecursiveCharacterTextSplitter with token-based sizing

The 90% pattern that works for most prose.

Combine RecursiveCharacterTextSplitter (which tries multiple separators) with a token-based length function so chunk sizes are in tokens not characters.

recursive_token_splitter.py
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
The 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.
Fix #2

Use format-specific splitters for structured content

Markdown, HTML, and code deserve splitters that respect their structure.

For non-plain-text documents, format-aware splitters preserve headers, code blocks, and semantic boundaries. Chunk quality improves dramatically.

format_aware_splitters.py
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.
Fix #3

Use SemanticChunker when structure is inconsistent

For documents where paragraph boundaries do not align with topic boundaries.

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.

semantic_chunking.py
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)
Semantic chunking is not universally better — it can produce huge or tiny chunks on documents whose structure is already good. A/B test against RecursiveCharacterTextSplitter on a labeled test set.

Prevention checklist

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

  • Use RecursiveCharacterTextSplitter as the default; not CharacterTextSplitter.
  • 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

Depends on corpus. Sensible defaults: 500-1000 tokens with 100-200 overlap for general prose; 800-1500 for technical content; 200-400 for FAQ-style short answers. Always A/B test.
Yes — every overlapped section is stored (and embedded) twice. A 20% overlap increases storage 20%. For most vector stores this is negligible; for very large corpora, budget for it.
Before. Embed each chunk. Storing whole documents and chunking at query time defeats the purpose of a vector store — you cannot benefit from per-chunk similarity matching.
Yes with SemanticChunker. Or apply different splitters to different content types (short bio: keep whole; long article: split). Just track chunk size distribution — surprises hurt.
A powerful pattern. Store small chunks for embedding + retrieval; on hit, return the containing larger chunk to the LLM. LangChain's ParentDocumentRetriever implements this natively.

Get the weekly AI-error digest

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