RAG Latency Optimization
A slow RAG request is rarely slow everywhere. Time every stage, read the distribution, and fix the stage with the biggest share of the mean. Percentiles do not add up, and a fix that helps the average can leave the tail unchanged.
Overview
Find the slow stage before you fix anything
A RAG app with a four-second p95 is rarely slow everywhere. Usually one or two stages hold most of the delay. The method is to time every stage on every request, read the distribution, and then fix the biggest contributor. Guessing wastes weeks. You can tune retrieval for a month and save 30 ms while the model takes two seconds.
A typical request runs these stages in order: query cleanup, embedding the query, vector search, reranking, building the prompt, generating the answer, and post-processing. Generation is often the longest, reranking and retrieval come next, and the rest are small. Your own mix is what matters, so measure it.
Two terms come up all the time. The p50 is the median: half of requests are faster. The p95 is the time that 95% of requests beat, so it describes the slow tail. A system with a p50 of 800 ms and a p95 of 4,000 ms has a tail problem, not a general speed problem, and tails have different causes, such as cold starts, retries and pauses.
This page covers measuring, reading the distribution, speeding up generation and retrieval, running stages in parallel, protecting against hangs, and weighing latency against cost. The sample numbers throughout are assumptions chosen to show the method. Replace them with your own traces. Related pages: caching in RAG and hybrid search.
A slow RAG request is rarely slow everywhere.
In this guide
- Find the slow stage before you fix anything
- Time every stage
- Read the distribution, not one request
- Speed up generation
- Cache whole answers
- Speed up retrieval
- Run independent stages together
- Put a timeout on every stage
- Match the cause to the fix
- Weigh latency against cost
- Things to try with RAG Latency Optimization
- What to remember about RAG Latency Optimization
- Questions people ask about RAG Latency Optimization
- RAG Latency Optimization: recap in one screen
Parameters
Visualisation
—Readout
What to watch
- A smaller model shrinks the generation bar the most.
- Running the keyword search in parallel hides it behind the vector path.
- A cache lowers the average and leaves the miss path as slow as before.
RAG Latency Optimization
Measure each stage of a RAG request, find the one that costs you time, and fix that one first.
Find the slow stage before you fix anything
A RAG app with a four-second p95 is rarely slow everywhere. Usually one or two stages hold most of the delay. The method is to time every stage on every request, read the distribution, and then fix the biggest contributor. Guessing wastes weeks. You can tune retrieval for a month and save 30 ms while the model takes two seconds.
A typical request runs these stages in order: query cleanup, embedding the query, vector search, reranking, building the prompt, generating the answer, and post-processing. Generation is often the longest, reranking and retrieval come next, and the rest are small. Your own mix is what matters, so measure it.
Two terms come up all the time. The p50 is the median: half of requests are faster. The p95 is the time that 95% of requests beat, so it describes the slow tail. A system with a p50 of 800 ms and a p95 of 4,000 ms has a tail problem, not a general speed problem, and tails have different causes, such as cold starts, retries and pauses.
This page covers measuring, reading the distribution, speeding up generation and retrieval, running stages in parallel, protecting against hangs, and weighing latency against cost. The sample numbers throughout are assumptions chosen to show the method. Replace them with your own traces. Related pages: caching in RAG and hybrid search.
Time every stage
You cannot improve what you do not measure. Give each request a trace that records how long each stage took. In real code, use a monotonic clock around each stage:
import time
from contextlib import contextmanager
class Trace:
def __init__(self):
self.ms = {}
@contextmanager
def stage(self, name):
start = time.perf_counter()
try:
yield
finally:
self.ms[name] = (time.perf_counter() - start) * 1000
trace = Trace()
with trace.stage("embedding"):
vector = embed(query)
with trace.stage("retrieval"):
chunks = search(vector)Ship these traces to your monitoring system and look at the aggregate. The block below takes one recorded trace and shows where the time went.
Read the distribution, not one request
One trace can mislead. Gather a few thousand and look at each stage's spread. The simulation below draws stage times from assumed normal distributions and then compares them.
The stage p95s add up to more than the total p95, because it is rare for every stage to be slow at once. So do not add percentiles. Compare each stage's share of the mean time instead. Here generation takes about three quarters of it, so it is the target.
Speed up generation
Generation is usually the largest part. Three techniques attack it from different sides.
Stream the answer. Send tokens as they are produced. The user sees the first word almost at once instead of waiting for the full text. Total time is unchanged, but the time to first token, which is what users feel, drops sharply.
Cap the output length. If answers average 150 tokens, a max_tokens of 300 stops rare runaway replies from stretching the tail.
Route by difficulty. Send simple lookups to a small, fast model and keep the larger one for hard questions. Test this carefully, because the tail may not improve the way you expect.
Look at the three columns. Routing cut the median and the mean sharply, and it also cuts cost. But the p95 got worse here, because the 30% of hard questions now go to a slower model, and they make up the whole tail. Routing wins on typical latency and cost. To improve the tail you must speed up the slow path itself.
Cache whole answers
For repeated or similar questions, a cache can skip the pipeline. Check for an exact match first with a hash lookup, then for a semantic match: an embedding close enough to a stored question. Exact matching catches repeated questions. Semantic matching catches paraphrases such as "how to reset password" and "password reset steps".
Cache at the response level, not the chunk level. A chunk cache saves retrieval but not generation, which is the expensive part. Expire entries and drop them when content changes, because a stale cache is worse than none. See caching in RAG for invalidation.
The cache lowers the average, but a miss is as slow as ever. It does not fix the p95 of the requests that miss.
Speed up retrieval
Three changes help, and each trades a little accuracy or effort.
Filter before searching. Apply access-control and metadata filters inside the vector search, not after. A post-filter must fetch many extra candidates and throw most away.
Tune the search width. With an HNSW index, the ef_search setting trades speed for recall. Lower values are faster and miss more. Measure recall at several settings on your own data.
Search in two passes. Use compact binary vectors for a fast first pass over many candidates, then rescore the best with full-precision vectors.
Run independent stages together
Some stages do not depend on each other. The query embedding and a cache lookup both need only the query. With hybrid search, the keyword search needs only the query too, so it can run while the embedding and vector search happen. Total time then follows the longest path, not the sum.
Put a timeout on every stage
Tail latency often comes from one stage that hangs or runs slowly. Give each stage a budget and a fallback. If reranking runs out of time, skip it and use the retrieval order. Quality dips a little and the user still gets an answer. This is graceful degradation.
The right budget is a trade-off. A tight one protects the tail and degrades more requests. The simulation shows both sides.
Match the cause to the fix
A p95 far above the p50 has specific causes. Cold starts, the first request after a pod scales up, need pre-warming. Garbage-collection pauses in some stores need tuned memory settings. Retries on transient errors need capped backoff. Lock contention during index updates may need read replicas. Find the cause from the traces before you pick a remedy.
Weigh latency against cost
A model that is ten times faster and ten times more expensive per token may not be an improvement. State every latency fix with its cost. Routing is often attractive because it lowers the typical latency and the bill together, as the run above showed. Just remember what it does to the tail.
Things to try with RAG Latency Optimization
- Set Model size to small. The generation bar shrinks and the total follows it.
- Turn off Reranker. Quality may drop, and the total falls by the reranker's share.
- Turn on Run searches in parallel. The keyword search overlaps the vector search.
- Raise Cache hit rate to 40. The average falls, and the uncached p95 does not move.
What to remember about RAG Latency Optimization
Time every stage, read the distribution and fix the stage with the biggest share of the mean. Stream the answer, cap its length, cache whole answers, run independent stages in parallel and give every stage a timeout. Do not add percentiles together, and check what each fix does to the tail.
Questions people ask about RAG Latency Optimization
What is the biggest source of latency? Usually generation. Check your own traces.
Does streaming reduce total latency? No. It lowers time to first token, which is what users feel.
How much does a semantic cache help? It depends on how often questions repeat. Measure your hit rate before you build one.
When is model routing not helpful? When most questions are hard, or when you care about the tail more than the average.
How do I measure time to first token? Record when the first token arrives and subtract the request start time.
What p95 should I target? It depends on the product. Interactive apps need to feel fast, and batch jobs can tolerate more. Set a goal from user research, not a rule of thumb.
RAG Latency Optimization: recap in one screen
- Measure every stage; the biggest share of the mean is your target.
- Percentiles do not add up, so compare shares of mean time.
- Stream, cap output, cache answers and run independent stages in parallel.
- Routing helps typical latency and cost, not necessarily the tail.
- Give every stage a timeout and a fallback.