Agno is already fast. The team claims 70x faster than LangChain, and in our benchmarks, that's not marketing—it's real.
But "fast" is relative.
When we first deployed our dispute resolution system, agent responses averaged 2.1 seconds. For a chatbot, that's fine. For real-time mediation where users expect near-instant responses, it's not.
We needed sub-200ms.
Getting there required specific patterns—some obvious, some counterintuitive. This post breaks down exactly what we did, with code examples you can apply to your own Agno deployments.
Why 200ms Matters
First, let's talk about why we cared about 200ms specifically.
The Research Is Clear
- • Under 100ms feels instant
- • 100-300ms feels responsive
- • 300-1000ms feels noticeable
- • Over 1000ms feels slow
For our dispute resolution system, users are in emotional situations. They're frustrated. They want answers. Every second of latency compounds that frustration.
But it's not just UX. At scale, latency compounds:
Before: 2.1 Seconds
2.1s × 15,000 cases × 8 calls
= 78 hours of waiting
After: 200ms
0.2s × 15,000 cases × 8 calls
= 6.7 hours of waiting
That's 71 hours of user time saved. Per year. For one system.
The Baseline: 2.1 Seconds
Let's start with what we were working with. Here's where the time went:
| Component | Time |
|---|---|
| Context serialization | 50ms |
| LLM call (GPT-4) | 1,400ms |
| Tool execution (sequential) | 500ms |
| Response parsing | 50ms |
| Overhead | 100ms |
| Total | 2,100ms |
GPT-4 was the biggest chunk, but we couldn't just "make GPT-4 faster." We needed to optimize everything we controlled.
Pattern #1: Parallel Tool Execution
The first win was obvious once we looked at it. Our agent called four tools sequentially:
Sequential Tool Calls
- 1. Retrieve contract (120ms)
- 2. Retrieve precedents (180ms)
- 3. Retrieve party history (100ms)
- 4. Check jurisdiction rules (100ms)
Total: 500ms
But these tools don't depend on each other. They can run in parallel.
After: Parallel Execution
agent = DisputeAnalysisAgent(
tool_execution_mode="parallel" # Enable parallel tools
)
# Total: 180ms (longest tool)Result: 500ms → 180ms. 64% reduction in tool execution time.
Pattern #2: Model Routing
Here's the counterintuitive one: not every task needs GPT-4.
Our dispute analysis had multiple sub-tasks:
- • Case classification (simple)
- • Entity extraction (medium)
- • Legal reasoning (complex)
- • Confidence scoring (simple)
GPT-4 is overkill for classification. A fine-tuned small model is faster AND cheaper.
Model Router Implementation
model = ModelRouter({
"classification": "gpt-3.5-turbo", # Fast, cheap
"extraction": "claude-3-haiku", # Good at structure
"reasoning": "gpt-4", # Complex tasks only
"scoring": "gpt-3.5-turbo", # Fast, cheap
})| Task | Before (GPT-4) | After (Routed) |
|---|---|---|
| Classification | 400ms | 80ms |
| Extraction | 350ms | 120ms |
| Reasoning | 500ms | 500ms |
| Scoring | 150ms | 40ms |
| Total LLM time | 1,400ms | 740ms |
Result: 1,400ms → 740ms. 47% reduction in LLM time.
For simple cases (60% of volume), we skip GPT-4 entirely: 1,400ms → 280ms
Pattern #3: Context Optimization
We were passing way too much context. For a complex case, that could be 15,000+ tokens of context.
Problem: More tokens = more latency. GPT-4's time-to-first-token scales with input size.
Build Minimal Context Per Task
def build_optimized_context(case: Case, task: str) -> str:
# Always include core identifiers
base = f"Case: {case.id} | Type: {case.dispute_type}"
if task == "classification":
return base + f"\nDescription: {case.description[:500]}"
elif task == "reasoning":
# Only for complex reasoning do we include more
relevant_docs = retrieve_relevant_chunks(case, top_k=3)
return base + f"\nRelevant Sections: {relevant_docs}"
return base
# Before: 10,000+ tokens
# After: 200-800 tokens depending on taskResult: Average 100ms reduction just from context optimization.
Pattern #4: Response Streaming with Early Exit
For many tasks, we don't need the full response. We can start processing as soon as we have enough.
Stream and Exit Early
async def get_classification(case: Case) -> str:
async for chunk in agent.stream(prompt):
# Check if we have enough to classify
if classification := try_parse_classification(chunk):
return classification # Exit early
# Fallback: parse complete response
return parse_classification(chunk)For classification tasks, we typically exit after 30-40% of the response is generated.
Result: 80ms → 35ms for classification. 56% reduction.
Pattern #5: Intelligent Caching
Many inputs are repeated or similar. Caching prevents redundant computation.
Layer 1: Exact Match Cache
Use LRU cache with prompt hashing. Hit rate: ~15%
Layer 2: Semantic Cache
For similar (not identical) prompts, use embedding-based caching with 0.95 similarity threshold. Hit rate: ~25%
Layer 3: Tool Result Caching
Tool results often don't change frequently. Cache contracts for 1 hour, rules for 5 minutes. Hit rate: ~60%
Result: Effective 30-40% reduction in average latency from caching.
Pattern #6: Connection Pooling & Keep-Alive
This one's boring but impactful. New connection = 100-200ms overhead.
Connection Pooling
http_client = httpx.Client(
http2=True,
limits=httpx.Limits(
max_connections=100,
max_keepalive_connections=20,
keepalive_expiry=30
)
)
agno.configure(
http_client=http_client,
connection_pool_size=100
)Result: 100-200ms reduction on first request, consistent low latency on subsequent requests.
Pattern #7: Async All The Way
If you're doing anything synchronous in your agent pipeline, you're leaving performance on the table.
Async Pipeline
async def process_case(case: Case) -> Result:
# Independent steps run in parallel
classification, entities = await asyncio.gather(
classify(case),
extract_entities(case)
)
# Dependent step runs after
analysis = await analyze(case, classification, entities)
return Result(classification, entities, analysis)Before: 80ms + 120ms + 500ms = 700ms
After: max(80ms, 120ms) + 500ms = 620ms
The Combined Result
| Optimization | Reduction | New Total |
|---|---|---|
| Baseline | — | 2,100ms |
| Parallel tools | -320ms | 1,780ms |
| Model routing | -660ms | 1,120ms |
| Context optimization | -100ms | 1,020ms |
| Streaming + early exit | -45ms | 975ms |
| Caching (30% hit rate) | -290ms | 685ms |
| Connection pooling | -150ms | 535ms |
| Async pipeline | -80ms | 455ms |
Wait—that's 455ms, not 200ms. Right. For the average case.
Simple Cases (60% of volume)
175ms
Classification 35ms + Extraction 60ms + Analysis 80ms
Complex Cases (40% of volume)
650ms
Includes GPT-4 for complex reasoning
Weighted Average: 365ms
With Caching: 255ms
We got to sub-200ms for simple cases, sub-300ms weighted average. 88% reduction from baseline.
The Optimization Priority
If you're optimizing Agno performance, here's the priority order:
1. Model routing — Biggest impact. Don't use GPT-4 for simple tasks.
2. Parallel tool execution — Easy win. Enable it.
3. Caching — Compound returns. Start with tool results.
4. Context optimization — Reduce tokens, reduce latency.
5. Connection pooling — One-time setup, permanent benefit.
6. Async pipeline — If you have independent steps.
7. Streaming + early exit — For classification/extraction tasks.
You don't need all of these. Implement 1-3 and you'll see dramatic improvement.
When NOT to Optimize
A word of caution: premature optimization is still a thing.
Don't optimize if:
- • Your latency is already acceptable for your use case
- • You're still iterating on agent behavior
- • You don't have production traffic yet
- • The complexity cost outweighs the latency benefit
Get it working first. Get it correct. Then make it fast.
Final Thought
Agno is fast. But "fast" is a starting point, not a destination.
The patterns here—model routing, parallel execution, caching, context optimization—aren't Agno-specific. They're general principles for building performant AI systems.
2 seconds to 200ms isn't magic. It's methodology.
Building performance-critical AI systems?
Let's talkWe've done this optimization dance across multiple production deployments.
