Streaming Output in LangChain
Implement token streaming through LCEL chains so your application displays each word as it arrives rather than waiting for the full response, improving perceived latency.
Streaming Output in LangChain is a free AI Engineering Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the AI Engineering Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Why Streaming Matters
Without streaming, users stare at a blank screen while waiting for the LLM to finish generating — which can take 5–30 seconds for long responses. With streaming, tokens appear as they are generated, providing immediate feedback. This dramatically improves perceived responsiveness. LangChain's LCEL propagates streaming through the entire chain automatically when you call .stream().
Basic Streaming with .stream()
Every LCEL chain exposes a .stream() method that returns an iterator of chunks. For a chain ending in StrOutputParser, each chunk is a string fragment. You iterate over the chunks and print or yield them as they arrive. The streaming happens at the HTTP level — each token from the OpenAI API is forwarded through the parser as soon as it arrives.
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
chain = (
ChatPromptTemplate.from_template('Explain {topic} in detail.')
| ChatOpenAI(model='gpt-4o-mini')
| StrOutputParser()
)
# Stream tokens to stdout
for chunk in chain.stream({'topic': 'quantum entanglement'}):
print(chunk, end='', flush=True)
print() # final newlineAsync Streaming with .astream()
.astream() is the async version of .stream(). It returns an async iterator that you consume with async for. This is the correct approach in FastAPI, Starlette, and other async web frameworks where the request handler is a coroutine. Using sync streaming in an async handler would block the event loop.
import asyncio
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
chain = (
ChatPromptTemplate.from_template('Write a poem about {subject}')
| ChatOpenAI(model='gpt-4o-mini')
| StrOutputParser()
)
async def stream_response():
async for chunk in chain.astream({'subject': 'the ocean'}):
print(chunk, end='', flush=True)
asyncio.run(stream_response())Streaming in FastAPI with StreamingResponse
In FastAPI, you wrap an async generator in StreamingResponse with media_type='text/plain' to stream text tokens to the browser. For server-sent events (SSE), use media_type='text/event-stream' and format each chunk as data: ...\n\n. The browser then receives tokens as they are generated without waiting for the complete response.
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
async def generate_stream(topic: str):
async for chunk in chain.astream({'topic': topic}):
yield chunk
@app.get('/stream')
async def stream_endpoint(topic: str):
return StreamingResponse(
generate_stream(topic),
media_type='text/plain'
)
# SSE format for frontend EventSource
async def sse_stream(topic: str):
async for chunk in chain.astream({'topic': topic}):
yield f'data: {chunk}\n\n'Streaming Through Intermediate Steps
LCEL chains propagate streaming through each step that supports it. The StrOutputParser is streaming-aware and passes chunks through immediately. However, some parsers — like JsonOutputParser — must buffer the full output before parsing it, breaking streaming. LangChain makes this clear: if a step is not streaming-compatible, it accumulates the output before passing it downstream.
from langchain_core.output_parsers import JsonOutputParser
# This chain does NOT stream token by token
# JsonOutputParser must buffer the full response before parsing JSON
json_chain = (
ChatPromptTemplate.from_template('Return JSON: {task}')
| ChatOpenAI(model='gpt-4o-mini')
| JsonOutputParser() # buffers until complete
)
# But partial JSON streaming IS possible with streaming_json_parser
for partial in json_chain.stream({'task': 'list 3 colors'}):
print(partial) # prints partial dict as it fills inastream_events for Fine-Grained Control
.astream_events() provides a more granular streaming API that emits events for every step in the chain, not just the final output. Each event has a kind field (on_chain_start, on_llm_stream, on_chain_end) and a data payload. This lets you stream tool call results, intermediate reasoning, and final output separately to different parts of a UI.
async def stream_with_events(question: str):
async for event in chain.astream_events(
{'question': question},
version='v2'
):
kind = event['event']
if kind == 'on_llm_stream':
chunk = event['data']['chunk'].content
print(chunk, end='', flush=True)
elif kind == 'on_chain_end':
print('\n[Done]')
elif kind == 'on_tool_start':
print(f'\n[Tool: {event["name"]}]')Buffering Streamed Output
Sometimes you need to both stream tokens to the user and capture the complete response for logging or further processing. Use .astream() with a list accumulator. Join the chunks after the loop to get the full text. This pattern lets you display streaming output in real time while also storing the complete response for analytics, caching, or evaluation.
async def stream_and_capture(question: str) -> str:
full_response = []
async for chunk in chain.astream({'question': question}):
print(chunk, end='', flush=True) # stream to user
full_response.append(chunk) # also collect
print() # newline
complete = ''.join(full_response)
await log_response(question, complete) # log full text
return completeStreaming with Tool Calls
When a model generates a tool call in a streamed response, the function arguments arrive as token fragments. You must buffer the JSON argument string until the tool call is complete before executing it. LangChain handles this automatically in its agent executors, but if you are building a custom streaming loop, you must check the finish_reason and accumulate tool_call.function.arguments fragments.
from openai import AsyncOpenAI
client = AsyncOpenAI()
async def stream_with_tools(prompt: str):
tool_call_buffer = {}
async with client.chat.completions.stream(
model='gpt-4o-mini',
messages=[{'role': 'user', 'content': prompt}],
tools=[weather_tool_schema]
) as stream:
async for chunk in stream:
delta = chunk.choices[0].delta
if delta.tool_calls:
for tc in delta.tool_calls:
idx = tc.index
if idx not in tool_call_buffer:
tool_call_buffer[idx] = ''
if tc.function.arguments:
tool_call_buffer[idx] += tc.function.argumentsCancellation and Timeout with Streaming
Long streaming responses need cancellation support. In async Python, you can cancel an asyncio.Task wrapping the stream. In FastAPI, the framework handles client disconnect cancellation automatically when using StreamingResponse. Set a timeout via the OpenAI client's timeout parameter or wrap the stream with asyncio.wait_for() to abort after a maximum duration.
import asyncio
async def stream_with_timeout(question: str, timeout: float = 30.0):
async def _stream():
async for chunk in chain.astream({'question': question}):
yield chunk
try:
async for chunk in asyncio.timeout(_stream(), timeout):
print(chunk, end='', flush=True)
except asyncio.TimeoutError:
print('\n[Stream timed out after 30 seconds]')
except asyncio.CancelledError:
print('\n[Stream cancelled by client disconnect]')Client-Side SSE with JavaScript
On the frontend, the browser's native EventSource API consumes server-sent events. When the FastAPI endpoint emits data: token\n\n chunks, the EventSource fires a message event for each one. Append each token to the DOM as it arrives to create a typewriter effect. For more control, fetch() with response.body.getReader() gives you full streaming access.
// Frontend JavaScript (not Python)
const source = new EventSource('/stream?topic=quantum+computing');
const outputDiv = document.getElementById('output');
source.onmessage = (event) => {
outputDiv.textContent += event.data;
};
source.onerror = () => {
source.close();
outputDiv.textContent += ' [done]';
};
// Alternative: fetch with ReadableStream
const response = await fetch('/stream?topic=ai');
const reader = response.body.getReader();
while (true) {
const {done, value} = await reader.read();
if (done) break;
outputDiv.textContent += new TextDecoder().decode(value);
}Streaming Best Practices
Follow these best practices when implementing streaming: always use flush=True when printing to stdout to prevent buffering. Set stream_usage=True if you need accurate token counts during streaming. Emit a data: [DONE]\n\n sentinel at the end of SSE streams so the client knows when to close the connection. Test streaming endpoints with curl --no-buffer to verify tokens arrive incrementally.
# Complete SSE endpoint with DONE sentinel
async def sse_generator(question: str):
try:
async for chunk in chain.astream({'question': question}):
# Escape any newlines in the chunk
safe_chunk = chunk.replace('\n', ' ')
yield f'data: {safe_chunk}\n\n'
finally:
yield 'data: [DONE]\n\n'
@app.get('/chat/stream')
async def chat_stream(question: str):
return StreamingResponse(
sse_generator(question),
media_type='text/event-stream',
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'}
)Quick Check
Test your understanding of streaming output in LangChain.
Lesson Recap
In this lesson you learned: stream() and astream() let you iterate over token chunks as they are generated, eliminating the long wait for the full response, StreamingResponse in FastAPI with SSE format delivers tokens to browser clients in real time, and astream_events() provides fine-grained event hooks for each step in the chain including tool calls and intermediate outputs. Next up we explore memory management for multi-turn conversations.
Frequently asked questions
Is the “Streaming Output in LangChain” lesson free?
Yes — the full text of “Streaming Output in LangChain” is free to read here on the web, and the AI Engineering Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the AI Engineering Academy course, upgrade to CoddyKit PRO.
What will I learn in “Streaming Output in LangChain”?
Implement token streaming through LCEL chains so your application displays each word as it arrives rather than waiting for the full response, improving perceived latency. You practise AI Engineering Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start AI Engineering Academy?
No prior experience is required. AI Engineering Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Streaming Output in LangChain” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this AI Engineering Academy lesson?
Yes. Every AI Engineering Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- LangChain Architecture and Core Abstractions
- Building Chains with LCEL
- Branching and Parallel Chains
- Streaming Output in LangChain