n4nAI

Chunking strategy and token limits for RAG applications

Practical guide to designing a chunking strategy and token limits for RAG pipelines, with code for token-aware splitting and retrieval budgeting.

n4n Team4 min read893 words

Audio narration

Coming soon — every post will get a voice note here.

Most retrieval-augmented generation failures trace back to a weak chunking strategy token limits RAG plan rather than the model itself. If your chunks overflow the embedding model’s max input or starve the LLM of room to reason, retrieval returns fragments that look relevant but lose the surrounding context.

Why chunk size is a token budget problem

Chunking is not about characters or pages. It is about tokens: the atomic unit your embedding model and completion model both consume. Every embedding model has a hard input cap (OpenAI’s text-embedding-3-small accepts 8191 tokens, older ada-002 capped at 8191 as well, while some open-weight models sit at 512). Exceed it and the SDK silently truncates or errors. On the generation side, a 128k context window does not mean you should fill it with 100k of retrieved text—attention degrades and cost scales linearly.

A defensible chunking strategy token limits RAG approach starts by treating the document as a token stream and the pipeline as a fixed budget split across embed, retrieve, and generate phases.

Step 1: Profile documents and model constraints

Before writing a splitter, measure. Pull a representative sample of your corpus and tokenize it. Use the same tokenizer the embedding model uses—for OpenAI models that is cl100k_base via tiktoken.

import tiktoken

enc = tiktoken.get_encoding("cl100k_base")

def token_len(text: str) -> int:
    return len(enc.encode(text))

sample = open("contract.pdf.txt").read()
print(f"doc tokens: {token_len(sample)}")

Record three numbers: median doc length, p95 doc length, and the longest single logical section (e.g., a clause or function). Compare against your embedding model max tokens and your completion model’s context window minus expected output tokens.

Step 2: Choose chunk size and overlap from query granularity

Queries in RAG are usually short and target a specific fact. If a user asks “What is the termination penalty?” the answer likely lives in a 200–400 token span. Chunks of 1024 tokens with 10% overlap work for prose; code or tables often need smaller, structure-aware chunks.

Rule of thumb:

  • Prose: 256–512 tokens, 15% overlap
  • Technical docs: 128–256 tokens, 20% overlap
  • Source code: per-function or 64–128 tokens, no blind character split

Overlap prevents boundary cuts but costs storage and can cause duplicate hits. Cap overlap so retrieved set deduplication is trivial.

CHUNK_TOKENS = 384
OVERLAP_TOKENS = 64

def chunk_by_tokens(text, chunk_t=CHUNK_TOKENS, overlap_t=OVERLAP_TOKENS):
    tokens = enc.encode(text)
    step = chunk_t - overlap_t
    for i in range(0, len(tokens), step):
        yield enc.decode(tokens[i:i+chunk_t])

Step 3: Split on structure, not just characters

Character splitters that cut every N tokens ignore headings, list items, and code fences. A sentence mid-way through a numbered list loses its referent. Prefer recursive splitting that respects separators in priority order.

SEPS = ["\n## ", "\n### ", "\n\n", "\n", ". ", " "]

def recursive_split(text, max_tok):
    if token_len(text) <= max_tok:
        return [text]
    for sep in SEPS:
        if sep in text:
            left, right = text.split(sep, 1)
            # reattach sep to right for context
            right = sep + right
            return recursive_split(left, max_tok) + recursive_split(right, max_tok)
    # fallback: hard token cut
    return list(chunk_by_tokens(text, max_tok, 0))

For Markdown, keep heading ancestry in each chunk: prepend the H1/H2 path so the embedding carries section context. This lifts recall without increasing token count.

Step 4: Enforce limits at ingest and query time

Never assume upstream data is clean. At ingest, reject or split any block exceeding the embedding limit. At query time, compute how many chunks fit in the completion context.

SYS_TOK = 120
QUERY_TOK = 64
OUTPUT_RESERVE = 1024
MODEL_CTX = 32768

def max_retrieval_tokens(model_ctx=MODEL_CTX):
    return model_ctx - SYS_TOK - QUERY_TOK - OUTPUT_RESERVE

def chunks_to_fetch(chunk_tok, k):
    budget = max_retrieval_tokens()
    return min(k, budget // (chunk_tok + 8))  # 8 tok overhead per chunk metadata

If you retrieve with a vector DB that returns scores, apply this k cap before sending to the model.

Step 5: Map chunk tokens to a retrieval and generation budget

A common pitfall is setting k=10 because a tutorial did. With 512-token chunks that is 5k tokens—fine for a 32k model, dangerous for an 8k one. Build the budget explicitly:

{
  "embedding_model_max_tokens": 8191,
  "chunk_tokens": 384,
  "overlap_tokens": 64,
  "index_overhead_tokens_per_chunk": 8,
  "completion_model_context": 32768,
  "system_prompt_tokens": 120,
  "query_tokens": 64,
  "output_reserve_tokens": 1024,
  "max_k": 48
}

Recalculate max_k whenever you swap models. This is where an inference gateway helps: if you route through an OpenAI-compatible endpoint like n4n.ai that honors client routing directives and forwards provider cache-control hints, you can change the completion model without rewriting chunk-size logic in every service.

Common pitfalls in chunking strategy token limits RAG

Silent truncation at embed time. The embedding API drops tokens beyond its limit. Your index quietly misses the second half of long clauses. Always pre-split.

Overlap inflation. 30% overlap on 10k chunks means you store 30% more vectors and pay 30% more query compute for marginal recall gain. Measure recall@k on a labeled set; tune overlap down until it drops.

Ignoring metadata tokens. If you inject source: file.pdf page 12 per chunk, that counts. A 384-token chunk plus 32 tokens metadata becomes 416. Update the budget.

Retrieving by fixed k, not by token sum. Two chunks of 200 tokens and one of 900 should not be treated identically. Sort by score, then fill the token budget greedily.

No separation of embed vs completion tokenizer. Using cl100k_base for a Cohere embed model is wrong; mismatch causes off-by-2x token counts. Check the model card.

Structure-aware chunking for code RAG

Code does not obey prose rules. A function is the unit. Use a parser (tree-sitter or language server) to extract top-level definitions, then chunk only if a single definition exceeds the limit.

# pseudo: tree-sitter extraction
from tree_sitter import Parser, Language

parser = Parser()
parser.set_language(Language("build/python.so", "python"))

def extract_functions(source):
    tree = parser.parse(bytes(source, "utf8"))
    # walk tree for function_definition nodes, return node.text
    ...

If a function is 1200 tokens, fall back to splitting by logical blocks (loop, branch) with overlap that includes the function signature.

A worked end-to-end config

Below is a minimal ingest pipeline that respects token limits and emits overlap with heading context.

import re, tiktoken
enc = tiktoken.get_encoding("cl100k_base")

def add_heading_context(text, chunk):
    heads = re.findall(r"(?m)^(#{1,2})\s+(.+)$", text)
    prefix = " > ".join(h[1] for h in heads[:2])
    return f"{prefix}\n\n{chunk}" if prefix else chunk

def ingest(doc_text, chunk_t=384, overlap_t=64):
    chunks = []
    for sec in recursive_split(doc_text, chunk_t):
        toks = enc.encode(sec)
        for i in range(0, len(toks), chunk_t - overlap_t):
            piece = enc.decode(toks[i:i+chunk_t])
            chunks.append(add_heading_context(doc_text, piece))
    return chunks  # each under embed limit, ready for vector store

At query time, compute k from the live completion model context and fetch that many. Log the actual token sum per request to catch drift.

What to measure next

Track three metrics from day one: mean retrieved token sum, recall@k on a 50-question gold set, and p95 time-to-first-token with full context. When recall plateaus but latency climbs, reduce chunk_t and increase k instead of padding chunks. The right chunking strategy token limits RAG tuning is iterative, not a one-time config.

If your pipeline spans multiple providers, keep the token math in one module and pass model identifiers explicitly. That isolates the only part of the system that must change when a new model with a different context window appears.

Tagschunkingragtoken-limitsguide

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All best api for rag pipelines posts →