Streaming Responses AI Agents Implementation: Building Real-Time, Responsive AI Systems
Learn how to implement streaming responses in AI agents for real-time user experiences. Handle token-by-token generation, manage state, and build responsive systems users love.

Users abandon AI interfaces that stare back at them with a blank screen. Streaming responses fix this by showing the model's output token by token, so the user sees the first word in a fraction of a second instead of waiting for the full answer. This guide walks through a complete streaming responses AI agents implementation: the transport layer, the LLM client calls, state management, tool calls, frontend rendering, and the mistakes that break streams in production.
Why streaming changes perceived performance
A non-streaming agent works like this: the user sends a message, the backend waits for the model to generate the entire response, then returns it. A five-second generation is five seconds of dead air, and the user cannot tell a slow answer from a crashed server.
Streaming splits that wait. The metric that matters is time to first token (TTFT) — the elapsed time between submitting a prompt and receiving the first token of the response. TTFT includes network transit, server-side queueing, and the prefill phase where the model processes your whole prompt, which means it grows with prompt length (ClickHouse, LLM inference latency).
The reason streaming feels so much faster: users see the first content at TTFT rather than at end-to-end latency, often a 10–30x difference in time-to-first-content. Everything generated faster than reading speed is effectively free. ClickHouse's worked example makes this concrete — a request with a 400ms TTFT that takes 12.7 seconds to finish only ever "waits" for 400ms from the user's point of view. Once tokens arrive faster than a person reads, the remaining latency disappears.
As rules of thumb: TTFT under roughly 300ms feels instant, under a second feels responsive, and beyond two seconds of silence users start to assume failure. TTFT plus inter-token latency (how fast subsequent tokens arrive) are the two numbers to instrument — both are standard metrics in serving stacks like NVIDIA NIM.
Choosing the transport: SSE vs WebSocket
Server-Sent Events for most agents
SSE is a one-way stream from server to browser over plain HTTP. It is the right default for chat-style agents because it is simple, auto-reconnects in browsers, and works through most proxies:
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
@app.post("/stream")
async def stream_response(query: str):
async def generate():
async for chunk in agent_stream(query):
# SSE frames: "data: <payload>\n\n"
yield f"data: {chunk}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(generate(), media_type="text/event-stream")
Practical SSE notes:
- Disable buffering in any reverse proxy. For nginx that means
X-Accel-Buffering: noon the response, or chunks will arrive in bursts. - Send heartbeats (a comment line like
: ping\n\nevery 15–30 seconds) so idle connections are not killed by load balancers. - Use POST, not GET, if your query is long or contains sensitive data — the browser
EventSourceAPI only does GET, so for POST you read the stream withfetch()and aReadableStreamreader instead.
WebSocket for bidirectional agents
Use WebSocket when the client also streams — voice input, live cursor position, collaborative editing, or mid-generation interrupts:
from fastapi import WebSocket, WebSocketDisconnect
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
try:
while True:
user_message = await websocket.receive_text()
async for chunk in agent_stream(user_message):
await websocket.send_json({"type": "chunk", "content": chunk})
await websocket.send_json({"type": "done"})
except WebSocketDisconnect:
pass # client left; cancel any running generation
WebSocket costs more to operate (sticky connections, manual reconnection, harder load balancing), so do not reach for it unless you genuinely need two-way streaming. Voice agents are the clearest case — our guide to AI voice agents for business covers that architecture in more detail.
Streaming from the LLM providers
OpenAI
The Responses API is OpenAI's current recommended surface, and streaming is a single flag. Events arrive as typed objects you iterate over:
from openai import AsyncOpenAI
client = AsyncOpenAI()
async def stream_chat(messages):
stream = await client.responses.create(
model="gpt-5.2",
input=messages,
stream=True,
)
async for event in stream:
if event.type == "response.output_text.delta":
yield event.delta
If you are on the older Chat Completions endpoint, pass stream=True and read chunk.choices[0].delta.content; add stream_options={"include_usage": True} to receive token counts in a final chunk — useful for cost tracking. See the OpenAI streaming reference for the full event list.
Anthropic
The Anthropic SDK wraps streaming in a context manager that yields text deltas and accumulates the final message for you:
from anthropic import AsyncAnthropic
client = AsyncAnthropic()
async def stream_claude(prompt):
async with client.messages.stream(
model="claude-sonnet-5-5",
max_tokens=2048,
messages=[{"role": "user", "content": prompt}],
) as stream:
async for text in stream.text_stream:
yield text
# After the loop: full message with usage stats
final = await stream.get_final_message()
Model IDs move fast — claude-3-5-sonnet-20241022 from older tutorials is long superseded; check the Anthropic streaming docs for current model names and event types (text deltas, tool-use deltas, and thinking deltas all stream as separate events).
Streaming through agent frameworks
LangChain
Modern LangChain (0.2+) exposes two streaming levels, per the LangChain streaming how-to:
astream— streams the final output of a chain token by token.astream_events— streams intermediate steps too: tool starts, tool ends, retriever output, and model tokens, each as a typed event.
Token streaming from a chat model directly (note the import — langchain.chat_models is deprecated in favor of the provider packages):
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-5.2", streaming=True)
async def stream_tokens(query):
async for chunk in llm.astream(query):
if chunk.content:
yield chunk.content
Streaming tool calls and agent events
Agents that call tools mid-generation need astream_events so the UI can show "searching..." states instead of freezing:
async def stream_agent(query):
async for event in agent.astream_events(query, version="v2"):
kind = event["event"]
if kind == "on_chat_model_stream":
chunk = event["data"]["chunk"]
if chunk.content:
yield chunk.content
elif kind == "on_tool_start":
yield f"\n*[Using tool: {event['name']}...]*\n"
elif kind == "on_tool_end":
yield "*[Done]*\n"
This "show the working" pattern matters more than it looks. Tool calls are the moments where TTFT balloons — the model pauses while a search or database query runs — and a visible status update is the difference between "thinking" and "broken."
State management while streaming
Chunks arrive before the response is complete, which creates a consistency question: when do you persist the conversation?
Buffer, then write. Accumulate the full response, stream each chunk to the user, and write to your database only after the stream finishes:
async def stream_with_state(query, user_id):
full = ""
async for chunk in llm.astream(query):
full += chunk
yield chunk
await save_turn(user_id, query, full) # one write, complete record
This is the right default: one write, no partial rows, and your conversation history always contains complete messages.
Incremental writes (append each chunk to the session store as it arrives) only make sense when a crash must not lose progress — very long generations, or resumable sessions. The cost is partial state everywhere, so you also need a "finalized" flag and cleanup for abandoned streams.
Either way, handle cancellation: when the client disconnects mid-stream, cancel the upstream LLM call too, or you pay for tokens nobody reads.
Frontend rendering
Consuming the stream
With EventSource for GET-based SSE, or fetch for POST:
async function streamResponse(query: string, onChunk: (t: string) => void) {
const res = await fetch("/stream", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query }),
});
const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += value;
// SSE frames are separated by blank lines
const frames = buffer.split("\n\n");
buffer = frames.pop() ?? "";
for (const frame of frames) {
const data = frame.replace(/^data: /, "");
if (data === "[DONE]") return;
onChunk(data);
}
}
}
Always include a stop button wired to AbortController — users routinely bail on long answers, and aborting should cancel the server-side generation too.
Rendering incomplete Markdown
A half-streamed response is half-valid Markdown: unclosed code fences, dangling bold markers, cut-off tables. Naive rendering flickers on every chunk. The practical fix:
- Throttle renders (every 50–100ms, not every token).
- Before rendering, close open code fences — if the count of
```markers is odd, append a closing fence. - Prefer a streaming-aware Markdown renderer that handles partial input, rather than sanitizing by hand.
Metrics that tell you the stream is healthy
Track these per request (as a list, because numbers in tables rot):
- Time to first token — target under ~300ms for interactive chat; the single biggest driver of perceived speed.
- Inter-token latency / tokens per second — fast enough that output stays ahead of reading speed (roughly 20+ tokens/second for English prose).
- Stream completion rate — share of streams that finish without client disconnect or server error; drops here usually mean proxy timeouts or OOMs.
- Upstream vs downstream TTFT gap — the difference between when your server got the first token and when the client did; a growing gap means buffering somewhere in your stack.
These belong in the same dashboards as the rest of your agent telemetry — see our production guide to AI agent monitoring and observability for how to wire that up.
When streaming is the wrong choice
Streaming is not free complexity, and some workloads should stay request-response:
- Structured output you must validate. If the response is JSON that gets parsed and acted on, a partial stream is useless — validate the complete object, then show it. Stream only if the UI renders progressively (like code appearing line by line).
- Content moderation gating. If every response must pass a safety filter before the user sees any of it, streaming defeats the gate. OpenAI's own documentation notes that partial completions are harder to moderate.
- Batch and pipeline work. Nightly document processing, extraction jobs, and agent-to-agent calls have no human watching. Streaming there just adds failure modes.
- Very short answers. If the full response arrives in 300ms anyway, the streaming machinery buys nothing.
A related judgment call: agents in customer-facing automation usually benefit from streaming because a human is waiting, which is why it shows up in most customer service agent deployments. Internal automation usually does not.
Common pitfalls
- Proxy buffering — nginx, Cloudflare, or your framework's gzip middleware holds chunks and flushes them in bursts. Test through the full production path, not localhost.
- No heartbeat on idle streams — connections die silently during long tool calls. Send keepalives.
- Leaking generations — client disconnects but the LLM call runs to completion. Cancel upstream on disconnect.
- No loading state before the first token — TTFT still exists; show a "thinking" indicator until the first chunk lands.
- Rendering every token — re-rendering Markdown 50 times a second janks the UI. Batch updates.
- Unbounded responses — set
max_tokensand a client-side length cap, or one runaway generation burns budget and memory. - Retrying mid-stream naively — if a stream dies after 200 tokens, blindly restarting duplicates content. Either restart cleanly or resume with the partial text in the prompt.
Frequently asked questions
Does streaming make the model faster?
No. Total generation time is the same or slightly higher. Streaming changes when the user sees output — first content at TTFT instead of at full completion, often a 10–30x improvement in time-to-first-content. Perceived speed is what improves.
Should I use SSE or WebSocket for a chatbot?
SSE, almost always. It is one-directional (server to client), which is exactly what token streaming needs, and browsers reconnect it automatically. Reserve WebSocket for genuinely bidirectional cases like live voice or real-time collaboration.
How do I stream responses that include tool calls?
Use an event-level streaming API — LangChain's astream_events, or the provider's native event stream — and render tool start/end events as status messages in the UI. The model's text pauses while tools run, so a visible "searching..." state keeps the interface from looking frozen.
What is a good time to first token?
Under about 300ms feels instant; under one second is fine for most chat products. Remember that TTFT grows with prompt length because the model must prefill the entire prompt before generating anything — a 100,000-token document in context will never have a 200ms TTFT.
Can I stream JSON or function-call arguments?
Yes — providers stream tool-call arguments as deltas that you accumulate and parse only when complete. Never try to parse partial JSON from mid-stream chunks.
Where to go from here
Start with the simplest version: one SSE endpoint, one buffered state write, one stop button. Get TTFT and completion rate into a dashboard before you optimize anything. If you would rather have engineers who have shipped this pattern before build the real-time layer for you — whether that is a Claude-based agent or a full custom application — look at our Claude implementation services or custom AI builds and talk to us about your latency budget.
About AI Agents Plus Editorial
AI automation expert and thought leader in business transformation through artificial intelligence.



