By Sagar Shankaran, Founder of CallSphere
Build a production-grade real-time dashboard for monitoring AI agents, featuring live metrics pipelines, streaming log aggregation, agent health indicators, and efficient frontend rendering with React.
Key takeaways
Monitoring AI agents in production requires more than traditional APM tools. You need to see token throughput, model latency percentiles, tool call success rates, agent reasoning traces, and cost accumulation — all updating in real time. A well-built dashboard transforms a black-box AI system into an observable one where you can spot degradation before users notice.
The architecture follows three layers: a metrics collection backend that aggregates data from running agents, a streaming transport layer that pushes updates to the browser, and a frontend that renders efficiently without choking on high-frequency updates.
Start by instrumenting your agents to emit structured events. Each event carries a timestamp, agent ID, event type, and a payload with type-specific data.
sequenceDiagram
autonumber
participant Client
participant Edge as Edge Worker
participant LLM as LLM Provider
participant DB as Logs and Trace
Client->>Edge: POST /chat (stream=true)
Edge->>LLM: messages.create(stream=true)
loop Each token
LLM-->>Edge: SSE chunk delta
Edge-->>Client: SSE chunk delta
Edge->>DB: append token to span
end
LLM-->>Edge: stop_reason=end_turn
Edge-->>Client: event: done
Edge->>DB: finalize trace
import asyncio
import time
import json
from dataclasses import dataclass, asdict
from typing import Optional
from collections import defaultdict, deque
@dataclass
class AgentMetricEvent:
agent_id: str
event_type: str # "token", "tool_call", "error", "completion"
timestamp: float
payload: dict
class MetricsAggregator:
def __init__(self, window_seconds: int = 60):
self.window = window_seconds
self.events: deque[AgentMetricEvent] = deque()
self.subscribers: list[asyncio.Queue] = []
def record(self, event: AgentMetricEvent):
self.events.append(event)
self._prune_old_events()
snapshot = self._compute_snapshot()
for queue in self.subscribers:
try:
queue.put_nowait(snapshot)
except asyncio.QueueFull:
pass # Drop if subscriber is slow
def _prune_old_events(self):
cutoff = time.time() - self.window
while self.events and self.events[0].timestamp < cutoff:
self.events.popleft()
def _compute_snapshot(self) -> dict:
now = time.time()
recent = [e for e in self.events if e.timestamp > now - self.window]
tokens = [e for e in recent if e.event_type == "token"]
tool_calls = [e for e in recent if e.event_type == "tool_call"]
errors = [e for e in recent if e.event_type == "error"]
completions = [e for e in recent if e.event_type == "completion"]
latencies = [
e.payload.get("latency_ms", 0) for e in completions
]
latencies.sort()
return {
"timestamp": now,
"tokens_per_second": len(tokens) / max(self.window, 1),
"tool_calls_total": len(tool_calls),
"error_rate": len(errors) / max(len(recent), 1),
"completions": len(completions),
"p50_latency_ms": latencies[len(latencies) // 2] if latencies else 0,
"p99_latency_ms": latencies[int(len(latencies) * 0.99)] if latencies else 0,
"active_agents": len(set(e.agent_id for e in recent)),
}
def subscribe(self) -> asyncio.Queue:
queue = asyncio.Queue(maxsize=100)
self.subscribers.append(queue)
return queue
def unsubscribe(self, queue: asyncio.Queue):
self.subscribers.remove(queue)
aggregator = MetricsAggregator(window_seconds=60)
The aggregator uses a sliding window deque for memory efficiency. Old events are pruned on each insertion, keeping memory usage bounded. Subscribers receive computed snapshots rather than raw events, reducing frontend processing load.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
For a monitoring dashboard, SSE is the right transport — the data flows one direction (server to browser), and we get automatic reconnection for free.
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
async def metrics_stream():
queue = aggregator.subscribe()
try:
while True:
snapshot = await queue.get()
data = json.dumps(snapshot)
yield f"event: metrics\ndata: {data}\n\n"
finally:
aggregator.unsubscribe(queue)
@app.get("/api/dashboard/stream")
async def dashboard_stream():
return StreamingResponse(
metrics_stream(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
Agent logs need their own stream. Structured log events let the frontend filter and highlight based on severity or agent ID.
from collections import deque
log_buffer: deque[dict] = deque(maxlen=1000)
log_subscribers: list[asyncio.Queue] = []
def emit_agent_log(agent_id: str, level: str, message: str, metadata: dict = None):
entry = {
"timestamp": time.time(),
"agent_id": agent_id,
"level": level,
"message": message,
"metadata": metadata or {},
}
log_buffer.append(entry)
for q in log_subscribers:
try:
q.put_nowait(entry)
except asyncio.QueueFull:
pass
async def log_stream():
queue = asyncio.Queue(maxsize=200)
log_subscribers.append(queue)
try:
# Send recent history first
for entry in log_buffer:
yield f"event: log\ndata: {json.dumps(entry)}\n\n"
# Then stream new entries
while True:
entry = await queue.get()
yield f"event: log\ndata: {json.dumps(entry)}\n\n"
finally:
log_subscribers.remove(queue)
Sending the recent buffer on connection lets newly opened dashboards see immediate context instead of staring at a blank screen.
High-frequency updates can overwhelm React if every SSE event triggers a re-render. Batch updates and use requestAnimationFrame to align rendering with the browser's paint cycle.
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.
import { useState, useEffect, useRef, useCallback } from "react";
interface DashboardMetrics {
tokens_per_second: number;
error_rate: number;
p50_latency_ms: number;
p99_latency_ms: number;
active_agents: number;
}
function useMetricsStream(url: string): DashboardMetrics | null {
const [metrics, setMetrics] = useState<DashboardMetrics | null>(null);
const latestRef = useRef<DashboardMetrics | null>(null);
const rafRef = useRef<number>(0);
const scheduleUpdate = useCallback(() => {
if (rafRef.current) return;
rafRef.current = requestAnimationFrame(() => {
rafRef.current = 0;
if (latestRef.current) {
setMetrics({ ...latestRef.current });
}
});
}, []);
useEffect(() => {
const source = new EventSource(url);
source.addEventListener("metrics", (event) => {
latestRef.current = JSON.parse(event.data);
scheduleUpdate();
});
return () => {
source.close();
if (rafRef.current) cancelAnimationFrame(rafRef.current);
};
}, [url, scheduleUpdate]);
return metrics;
}
This hook stores the latest event in a ref (no re-render) and schedules a single state update per animation frame. Even if the server sends 30 events per second, React only re-renders at the display refresh rate.
Use server-side aggregation to pre-compute summary statistics rather than pushing raw events to the browser. The MetricsAggregator pattern shown above computes totals and percentiles server-side, so the browser receives one compact snapshot per update regardless of how many agents are running. For drill-down views, let the user select specific agents and open filtered streams that only include events from those agents.
For production systems, persist metrics to a time-series database like TimescaleDB or InfluxDB alongside the in-memory aggregator. The in-memory layer serves real-time streaming, while the database provides historical data for trend analysis and post-incident investigation. On restart, the aggregator begins with an empty window and fills naturally within one window period (typically 60 seconds).
Build a metrics simulator that generates realistic event patterns — bursts of token events, periodic tool calls, occasional errors, and varying latency distributions. Run the simulator as a script that calls the same aggregator.record() method your real agents use. This lets you test the full pipeline including edge cases like error rate spikes and latency degradation without consuming API credits.
#Dashboard #RealTimeAI #Monitoring #React #Python #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.
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.
Step-by-step build of a working agent with the OpenAI Agents SDK — Agent class, tools, handoffs, tracing — plus an eval pipeline that catches regressions before merge.
Sentiment is not a single number per call - it is a curve. The shape (started positive, dropped at minute 4, recovered) tells you what your AI did wrong. Here is the per-utterance sentiment pipeline and the dashboards we ship by vertical.
Smolagents lets agents write Python instead of JSON. Why code-as-action reduces tool errors and where the security trade-offs are for production deployments.
How to wire Vercel AI SDK 5 tool calls to a React UI with streaming, partial UI updates, and proper error handling that survives flaky network conditions.
Sub-second agent decisions need explicit budgets at every step. The 2026 latency-engineering patterns from real production deployments.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.