By Sagar Shankaran, Founder of CallSphere
Learn how to create and enforce latency budgets for real-time AI systems, breaking down time allocation across network, preprocessing, inference, tool execution, and response delivery layers.
Key takeaways
A latency budget is a fixed time allocation for an end-to-end operation, divided among every component in the path. For a real-time AI agent that must respond within 2 seconds, you allocate specific millisecond budgets to network transit, request parsing, context retrieval, LLM inference, tool execution, and response delivery. If any component exceeds its budget, the overall target is at risk.
Without a latency budget, teams optimize blindly — shaving 5ms off database queries while the LLM inference takes 1,800ms of a 2,000ms total. Budgets force prioritization: you invest optimization effort where it has the highest impact relative to the allocation.
A typical AI agent request passes through these stages, each consuming a slice of the total latency:
flowchart LR
REQ(["Request"])
BATCH["Continuous batching<br/>vLLM scheduler"]
PREF{"Prefill or<br/>decode?"}
PRE["Prefill phase<br/>parallel attention"]
DEC["Decode phase<br/>token by token"]
KV[("Paged KV cache")]
SAMP["Sampling<br/>top-p, temp"]
STREAM["Stream tokens<br/>to client"]
REQ --> BATCH --> PREF
PREF -->|First token| PRE --> KV
PREF -->|Next token| DEC
KV --> DEC --> SAMP --> STREAM
SAMP -->|EOS| DONE(["Response complete"])
style BATCH fill:#4f46e5,stroke:#4338ca,color:#fff
style KV fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style STREAM fill:#0ea5e9,stroke:#0369a1,color:#fff
style DONE fill:#059669,stroke:#047857,color:#fff
| Stage | Description | Typical Range |
|---|---|---|
| Network ingress | Client to load balancer to application server | 5-50ms |
| Auth and validation | Token verification, input sanitization | 2-10ms |
| Context retrieval | RAG lookup, conversation history, user profile | 20-200ms |
| LLM inference | Time to first token (TTFT) | 200-2000ms |
| Tool execution | External API calls, database queries | 50-500ms per tool |
| Response assembly | Formatting, safety filtering | 5-20ms |
| Network egress | Server to client (first byte) | 5-50ms |
For a 2-second budget with one tool call, a realistic allocation might be: network 40ms, auth 5ms, context 100ms, inference 1200ms, tool 500ms, assembly 10ms, egress 20ms — totaling 1,875ms with 125ms of slack.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
Instrument every stage with precise timing to measure actual performance against the budget.
import time
from dataclasses import dataclass, field
from typing import Optional
from contextlib import asynccontextmanager
@dataclass
class LatencyBudget:
total_ms: float
allocations: dict[str, float] # stage -> max milliseconds
measurements: dict[str, float] = field(default_factory=dict)
start_time: Optional[float] = None
def start(self):
self.start_time = time.perf_counter()
@asynccontextmanager
async def track(self, stage: str):
stage_start = time.perf_counter()
try:
yield
finally:
elapsed_ms = (time.perf_counter() - stage_start) * 1000
self.measurements[stage] = elapsed_ms
def elapsed_ms(self) -> float:
if self.start_time is None:
return 0
return (time.perf_counter() - self.start_time) * 1000
def remaining_ms(self) -> float:
return max(0, self.total_ms - self.elapsed_ms())
def is_over_budget(self, stage: str) -> bool:
measured = self.measurements.get(stage, 0)
allocated = self.allocations.get(stage, float("inf"))
return measured > allocated
def report(self) -> dict:
return {
"total_budget_ms": self.total_ms,
"total_elapsed_ms": self.elapsed_ms(),
"within_budget": self.elapsed_ms() <= self.total_ms,
"stages": {
stage: {
"budget_ms": self.allocations.get(stage, None),
"actual_ms": round(self.measurements.get(stage, 0), 2),
"over_budget": self.is_over_budget(stage),
}
for stage in set(list(self.allocations) + list(self.measurements))
},
}
# Define budget for a standard agent request
def create_agent_budget() -> LatencyBudget:
return LatencyBudget(
total_ms=2000,
allocations={
"auth": 10,
"context_retrieval": 150,
"inference": 1400,
"tool_execution": 300,
"response_assembly": 20,
},
)
Integrate the budget tracker into your request handler so every stage is timed automatically.
from fastapi import FastAPI, Request
app = FastAPI()
@app.post("/api/agent/query")
async def agent_query(request: Request):
budget = create_agent_budget()
budget.start()
async with budget.track("auth"):
user = await authenticate(request)
async with budget.track("context_retrieval"):
context = await retrieve_context(
user_id=user.id,
timeout_ms=budget.allocations["context_retrieval"],
)
# Pass remaining budget to inference so it can set appropriate timeouts
async with budget.track("inference"):
inference_timeout = min(
budget.allocations["inference"],
budget.remaining_ms() - 50, # Reserve 50ms for response
)
result = await run_inference(
context=context,
timeout_ms=inference_timeout,
)
async with budget.track("tool_execution"):
if result.tool_calls:
tool_timeout = min(
budget.allocations["tool_execution"],
budget.remaining_ms() - 30,
)
tool_results = await execute_tools(
result.tool_calls,
timeout_ms=tool_timeout,
)
async with budget.track("response_assembly"):
response = assemble_response(result, tool_results)
# Log budget report for monitoring
report = budget.report()
if not report["within_budget"]:
log_latency_violation(report)
return response
The key technique is passing the remaining budget downstream. If context retrieval takes 200ms instead of the budgeted 150ms, the inference stage gets 50ms less — the budget adapts dynamically to prevent cascading delays.
When remaining budget is low, degrade gracefully rather than returning an error.
async def retrieve_context(user_id: str, timeout_ms: float) -> dict:
"""Retrieves context with graceful degradation under time pressure."""
context = {"conversation_history": [], "rag_results": [], "user_profile": {}}
# Always fetch conversation history (fast, essential)
try:
context["conversation_history"] = await asyncio.wait_for(
fetch_conversation(user_id),
timeout=timeout_ms / 1000 * 0.4, # 40% of budget
)
except asyncio.TimeoutError:
context["conversation_history"] = [] # Proceed without history
remaining = timeout_ms / 1000 * 0.5 # 50% for RAG
# RAG retrieval — skip if budget is too tight
if remaining > 0.02: # Only if > 20ms remaining
try:
context["rag_results"] = await asyncio.wait_for(
search_knowledge_base(user_id),
timeout=remaining,
)
except asyncio.TimeoutError:
pass # Proceed without RAG results
return context
This approach prioritizes essential data (conversation history) and treats expensive operations (RAG search) as optional under time pressure. The agent still responds — it just has less context to work with.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
Track budget compliance over time to identify degradation trends before they become user-visible problems.
from collections import defaultdict, deque
class LatencyMonitor:
def __init__(self, window_size: int = 1000):
self.window_size = window_size
self.reports: deque[dict] = deque(maxlen=window_size)
self.stage_violations: dict[str, int] = defaultdict(int)
def record(self, report: dict):
self.reports.append(report)
for stage, data in report.get("stages", {}).items():
if data.get("over_budget"):
self.stage_violations[stage] += 1
def get_p99_by_stage(self) -> dict[str, float]:
stage_latencies: dict[str, list[float]] = defaultdict(list)
for report in self.reports:
for stage, data in report.get("stages", {}).items():
if "actual_ms" in data:
stage_latencies[stage].append(data["actual_ms"])
result = {}
for stage, latencies in stage_latencies.items():
latencies.sort()
idx = int(len(latencies) * 0.99)
result[stage] = latencies[idx] if latencies else 0
return result
def violation_rate(self) -> float:
if not self.reports:
return 0
violations = sum(
1 for r in self.reports if not r.get("within_budget", True)
)
return violations / len(self.reports)
Start with your user experience target (e.g., 2 seconds for conversational AI, 5 seconds for complex analysis) and subtract fixed costs (network, auth, assembly). The remaining time is your inference budget. Measure your LLM provider's p50, p95, and p99 latencies for your typical prompt sizes. Set the budget at p95 — this means 5% of requests will exceed the budget, which you handle through graceful degradation or streaming. Track actual performance and adjust budgets quarterly as models and infrastructure change.
Streaming and budgets are complementary, not alternatives. Streaming improves perceived latency by showing tokens as they arrive, but you still need budgets for the non-streaming parts: context retrieval, tool execution, and time-to-first-token. A user sees nothing during TTFT, so that latency is fully perceived. Budget TTFT aggressively (under 500ms for conversational UX) and use streaming for the generation phase where users tolerate longer total times because they see progressive output.
Allocate a total tool execution budget and divide it among tool calls. If the budget is 500ms and the agent wants to call three tools, each gets roughly 165ms. Run independent tool calls in parallel using asyncio.gather to use the full 500ms for all three simultaneously. For sequential tool calls (where each depends on the previous result), enforce per-call timeouts and skip later calls if the budget is exhausted. Return partial results with a note that some tools were skipped due to time constraints.
#Latency #Performance #RealTimeAI #Optimization #Observability #AgenticAI #LearnAI #AIEngineering

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
Every 100ms of latency costs you. So does every cent per minute. Here is the decision matrix we use across 6 verticals to pick where to spend and where to save on voice AI infrastructure.
How to actually observe a WebSocket fleet: ping/pong heartbeats, Prometheus metrics that matter, dead-man switches, and the alerts that fire before customers notice.
WebTransport is Baseline as of March 2026. Media Over QUIC ships in production within the year. Here is what changes for AI voice agents — and what stays the same.
Vapi 465ms optimal, Retell 580-620ms, Bland ~800ms, ElevenLabs 400-600ms — but those are best-case. We design a fair benchmark harness, P95 measurement, and a reproducible methodology for 2026.
How to stream tokens, tool-call deltas, and intermediate steps from an agent — with code for both the OpenAI Agents SDK and LangChain — and the gotchas that bite in production.
How the modern agent eval stack actually flows: instrument, trace, dataset, evaluator, score, CI gate. The full pipeline that keeps agents from regressing.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.
Try Live DemoBook a DemoCalculate Your ROI