By Sagar Shankaran, Founder of CallSphere
How to build responsive AI applications using streaming, WebSockets, and SSE, with practical patterns for token streaming, agent status updates, and real-time collaboration.
Key takeaways
LLM inference is slow compared to traditional APIs. A complex query to a frontier model can take 5-30 seconds for the full response. Without streaming, users stare at a loading spinner for the entire duration. With streaming, they see tokens appear in real-time, dramatically improving perceived performance and user experience.
But token streaming is just the beginning. Production AI systems need real-time updates for agent status, tool execution progress, error notifications, and multi-user collaboration.
SSE is the most common pattern for LLM token streaming. It uses a standard HTTP connection with a special content type:
# FastAPI SSE endpoint
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import anthropic
app = FastAPI()
@app.post("/api/chat")
async def chat(request: ChatRequest):
async def generate():
client = anthropic.AsyncAnthropic()
async with client.messages.stream(
model="claude-sonnet-4-20250514",
messages=request.messages,
max_tokens=4096
) as stream:
async for event in stream:
if event.type == "content_block_delta":
yield f"data: {json.dumps({'text': event.delta.text})}\n\n"
# Send final message with usage stats
final = await stream.get_final_message()
yield f"data: {json.dumps({'done': True, 'usage': {'input': final.usage.input_tokens, 'output': final.usage.output_tokens}})}\n\n"
return StreamingResponse(
generate(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}
)
Client-side consumption:
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages })
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(l => l.startsWith('data: '));
for (const line of lines) {
const data = JSON.parse(line.slice(6));
if (data.text) appendToUI(data.text);
if (data.done) showUsageStats(data.usage);
}
}
SSE advantages: Simple, HTTP-based, works through most proxies and load balancers, automatic reconnection built into the EventSource API.
flowchart TD
HUB(("Why Real-Time Matters<br/>for AI"))
HUB --> L0["Token Streaming: The<br/>Foundation"]
style L0 fill:#e0e7ff,stroke:#6366f1,color:#1e293b
HUB --> L1["Choosing the Right Protocol"]
style L1 fill:#e0e7ff,stroke:#6366f1,color:#1e293b
HUB --> L2["Production Patterns"]
style L2 fill:#e0e7ff,stroke:#6366f1,color:#1e293b
style HUB fill:#4f46e5,stroke:#4338ca,color:#fff
flowchart LR
IN(["Input prompt"])
subgraph PRE["Pre processing"]
TOK["Tokenize"]
EMB["Embed"]
end
subgraph CORE["Model Core"]
ATTN["Self attention layers"]
MLP["Feed forward layers"]
end
subgraph POST["Post processing"]
SAMP["Sampling"]
DETOK["Detokenize"]
end
OUT(["Generated text"])
IN --> TOK --> EMB --> ATTN --> MLP --> SAMP --> DETOK --> OUT
style IN fill:#f1f5f9,stroke:#64748b,color:#0f172a
style CORE fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style OUT fill:#059669,stroke:#047857,color:#fff
flowchart TD
HUB(("Why Real-Time Matters<br/>for AI"))
HUB --> L0["Token Streaming: The<br/>Foundation"]
style L0 fill:#e0e7ff,stroke:#6366f1,color:#1e293b
HUB --> L1["Choosing the Right Protocol"]
style L1 fill:#e0e7ff,stroke:#6366f1,color:#1e293b
HUB --> L2["Production Patterns"]
style L2 fill:#e0e7ff,stroke:#6366f1,color:#1e293b
style HUB fill:#4f46e5,stroke:#4338ca,color:#fff
SSE limitations: Unidirectional (server to client only), limited to text data, connection limits per domain in browsers (6 in HTTP/1.1).
WebSockets provide full-duplex communication, essential for interactive agent sessions:
# FastAPI WebSocket for interactive agent
from fastapi import WebSocket
@app.websocket("/ws/agent")
async def agent_session(websocket: WebSocket):
await websocket.accept()
agent = create_agent(tools=available_tools)
while True:
user_message = await websocket.receive_json()
async for event in agent.run_stream(user_message["content"]):
match event.type:
case "thinking":
await websocket.send_json({
"type": "thinking",
"content": event.text
})
case "tool_call":
await websocket.send_json({
"type": "tool_call",
"tool": event.name,
"args": event.args,
"status": "executing"
})
case "tool_result":
await websocket.send_json({
"type": "tool_result",
"tool": event.name,
"result": event.result
})
case "text_delta":
await websocket.send_json({
"type": "text",
"content": event.text
})
WebSocket advantages: Bidirectional, low latency, supports binary data, client can send messages while receiving.
WebSocket limitations: More complex infrastructure (sticky sessions, WebSocket-aware load balancers), no automatic reconnection, connection management overhead.
| Use Case | Recommended Protocol |
|---|---|
| Simple chat with streaming | SSE |
| Interactive agent with tool use | WebSocket |
| Real-time collaboration | WebSocket |
| Notification/status updates | SSE |
| Voice/audio streaming | WebSocket |
| Webhook-style events | SSE |
Do not just stream raw text. Define an event protocol:
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.
type StreamEvent =
| { type: 'text_delta'; content: string }
| { type: 'tool_start'; tool: string; args: Record<string, unknown> }
| { type: 'tool_end'; tool: string; result: unknown; duration_ms: number }
| { type: 'thinking'; content: string }
| { type: 'error'; message: string; recoverable: boolean }
| { type: 'done'; usage: { input_tokens: number; output_tokens: number } };
This enables rich UI updates: show a spinner when a tool is executing, display thinking text in a collapsible panel, and show token usage when complete.
If the client cannot consume tokens as fast as the model generates them (common on slow networks), implement backpressure:
Connections drop. Your protocol should handle it:
# Server-side: assign event IDs for recovery
event_id = 0
async for token in stream:
event_id += 1
yield f"id: {event_id}\ndata: {json.dumps({'text': token})}\n\n"
# Client-side: reconnect with Last-Event-ID
const eventSource = new EventSource('/stream', {
headers: { 'Last-Event-ID': lastReceivedId }
});
proxy_buffering off and proxy_read_timeout settings for SSE. Use proxy_http_version 1.1 and Upgrade headers for WebSocketsessionAffinity: ClientIP for WebSocket services; increase proxy-read-timeout annotations for SSEStreaming is not just a UX nicety -- it is a fundamental requirement for AI applications. The difference between a 10-second loading spinner and seeing tokens appear immediately is the difference between an application users tolerate and one they enjoy.
Sources: MDN Server-Sent Events | FastAPI WebSocket Docs | Vercel AI SDK Streaming

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.
OpenAI's GPT-Realtime-Whisper launches at $0.017/min for streaming STT. Side-by-side latency, accuracy, and cost math vs Deepgram and the field.
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.
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.
Streaming changes the eval game — final-answer correctness isn't enough when users perceive the answer one token at a time. Here's the metric set that matters.
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.
The chat UI is half the user experience. The 2026 patterns for chat interfaces that surface LLM strengths and hide weaknesses.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco