Chunk size RAG latency is rarely analyzed as a first-class performance variable, yet it dictates the number of vectors, the breadth of search, and the volume of tokens shipped to the generator. Treat chunk size as a latency knob, not just a relevance knob, and you can cut p95 pipeline time without touching the model. This analysis traces the causal path from chunk boundaries to wall-clock time and shows where the tradeoffs actually bite.
The stages where chunk size bites
A RAG pipeline has four latency-sensitive stages that chunk size directly modifies: embedding, vector search, reranking, and generation. Skip any one and you misattribute the slowdown.
Embedding generation
The embedding model converts each chunk into a vector. Total embedding time is a function of token count and request overhead. If you split a 100,000-token corpus into 200-token chunks, you produce 500 vectors; at 1000-token chunks, you produce 100. The token count is identical, but the smaller chunks multiply HTTP round trips and batch padding waste unless you batch aggressively.
# Naive: one request per chunk
for chunk in chunks:
embed(chunk) # 500 calls
# Batched: one request for all
embed(chunks, batch_size=128) # 4 calls
Even batched, smaller chunks reduce average sequence length per item, which can lower GPU utilization because kernels favor longer sequences. You pay a tax in vector count regardless. Embedding endpoints often price per token but rate-limit per request; the request tax is the hidden latency line item.
Vector retrieval
More chunks mean a larger index and more distance computations per query if you retrieve top-k across the whole space. Approximate nearest neighbor (ANN) libraries like FAISS or DiskANN scale sublinearly, but metadata filtering and replication amplify cost. A 10x increase in chunk count can mean 2–3x query latency on brute-force cosine if you skip ANN indexing. Sharding by tenant multiplies that because each query hits more shards.
Reranking and post-processing
If you rerank the top 50 candidates with a cross-encoder, that model sees pairs of (query, chunk). Smaller chunks produce more candidates of shorter length; the cross-encoder’s latency is dominated by token count, so total rerank time may stay flat or drop slightly. But the preceding fetch of 50 vs 200 candidates adds overhead in the vector store and network serialization.
Context assembly and LLM generation
This is where chunk size RAG latency flips. Large chunks mean fewer retrieved pieces but each carries surplus text; you stuff 3000 tokens when 800 would answer. The generator pays prefill cost on every token. Small chunks let you retrieve precisely, but if you retrieve 10 chunks of 200 tokens each, you still send 2000 tokens. The difference is relevance density and the proportion of useless context the decoder must attend to on each step.
Why smaller chunks can increase latency
Engineers often assume tiny chunks are faster because each embedding is quick. The opposite happens at pipeline scale.
First, embedding APIs impose rate limits per request, not per token. Sending 500 single-chunk requests serially wastes milliseconds on TCP and auth overhead. Even with concurrency, you hit batch size ceilings and thread contention on the client.
Second, vector search indexes have fixed overhead per query. Querying a 1M-vector index is not 10x slower than 100k, but the client-side filtering and result merging across shards adds linear cost with chunk count when you scope searches by document ID. If your retrieval first fetches candidate chunks then expands to parent contexts, the expansion multiplies the fetched row count.
Third, retrieval logic frequently fetches top-k chunks, then expands to parent chunks for context. If your base chunk is 128 tokens and you expand to 512, you effectively doubled storage and embedding cost for no latency gain at generation.
{
"chunk_size": 128,
"overlap": 32,
"retrieve_k": 20,
"expand_to_parent": true
}
That config yields 20 retrieved units that balloon to 20 parents. The embedding stage processed 4x tokens relative to a 512 base chunk. The benchmark reveals the true chunk size RAG latency profile only when you instrument the expand step.
Why larger chunks can also increase latency
Large chunks are not a free win. They reduce vector count but shift cost to the generator.
A 2000-token chunk passed to the LLM as context requires the model to attend over all 2000 tokens for every generated token. Transformer prefill is O(n) in tokens; decode is O(n) per step. If your answer needs 100 tokens, prefill of 2000 tokens dominates. With 200-token chunks and precise retrieval, prefill drops to 400 tokens for the same answer.
Larger chunks also hurt cache hits. Many inference servers cache prefill by exact prefix. If you concatenate retrieved chunks into a system prompt, a 2000-token static chunk defeats prefix caching when only the last 200 tokens differ between queries. Smaller, fixed-size chunks combined with a stable instruction prefix let the gateway cache the prefix.
When forwarding retrieved context to an OpenAI-compatible endpoint, a gateway such as n4n.ai forwards provider cache-control hints, so stable system prompts paired with variable chunk content can hit prompt caches and avoid recomputing prefill for the instructions.
Additionally, oversized chunks dilute relevance. The retriever returns a blob that contains the answer but also off-topic text. The generator may then emit longer, meandering completions because it tries to cover the whole chunk, pushing decode latency up.
Measuring the tradeoff in practice
You cannot tune what you do not measure. Build a latency harness that varies chunk size and records each stage.
import time, statistics
def bench(chunk_size, docs, query):
t0 = time.time()
chunks = split(docs, chunk_size)
emb = embed_batch(chunks)
t_emb = time.time() - t0
t1 = time.time()
hits = index.search(emb, k=10)
t_search = time.time() - t1
t2 = time.time()
ctx = assemble(hits)
resp = llm_complete(ctx, query)
t_gen = time.time() - t2
return t_emb, t_search, t_gen
sizes = [128, 256, 512, 1024, 2048]
for s in sizes:
samples = [bench(s, corpus, q) for q in queries]
print(s, statistics.median(samples, key=lambda x: sum(x)))
Run this across sizes on a representative corpus. Plot p50 and p95. You will typically see embedding time flat or slightly rising at small sizes due to overhead, search time creeping up with chunk count, and generation time falling as chunk size grows until relevance decays and the model rambles. The knee is usually where embedding overhead and generation prefill cross.
Interaction with model context windows and caching
Context window size is a constraint, not a target. If your chunk size is 4096 and you retrieve 5 chunks, you consume 20k tokens before the query. On a 32k model that leaves little room. On a 128k model, prefill cost still scales linearly with those 20k tokens.
Cache-control directives (cache_control: {"type": "ephemeral"} on a stable system block) are honored by some providers. Placing your instructions and a fixed retrieved-schema description in a cached block, then appending only the variable chunk text, keeps prefill cheap. Chunk size influences how much variable text you add; smaller chunks make the variable portion tighter and improve cache residency.
A decisive recommendation
Start with 512-token chunks and 64-token overlap for general text using modern embedding models (BGE, E5, text-embedding-3). Batch embed with size 256. Retrieve 8–12 chunks, then rerank top 30 if needed. Cap total retrieved tokens at 4096 before generation. This keeps chunk size RAG latency near the knee of the curve: embedding overhead manageable, search index modest, generation prefill bounded.
If your documents are code or dense tables, drop to 256 tokens because semantic boundaries matter more than throughput. If your generator is a small local model, prefer larger chunks (1024) to reduce number of retrieved items and avoid many cross-attention layers over sparse small chunks.
The tradeoff is real but solvable. Measure stage-by-stage, batch the embedding, cap the context, and treat chunk size as a latency parameter first, a relevance parameter second.