LangChain .stream() returning full output at once instead of chunks (2026) — Fix Guide (2026) | AI Error Hub
Home Providers LangChain Streaming not chunking
LangChain LCEL · Streaming Severity: Medium HTTP n/a

LangChain .stream() — output arrives as one giant chunk

Streaming in LangChain is a chain-wide property: it works only when every step supports streaming natively. A single buffering step turns your beautiful token-by-token stream into a one-shot delivery.

By Ahmed R. · Senior AI Infrastructure Engineer Published 2026-07-26 Verified 2026-07-26

Quick fix (TL;DR)

Resolution: LangChain .stream() / .astream() yields incremental chunks only when every runnable in the chain implements streaming. Common buffering culprits: PydanticOutputParser, with_structured_output() with method="function_calling", custom RunnableLambda, and JsonOutputParser on non-array roots. Fix by (a) using StrOutputParser where structure is not needed, (b) using .astream_events() to inspect what each step emits, and (c) rewriting custom lambdas as generators.

Real error messages you'll see

These are the exact strings returned by the LangChain framework and its integrations when this error occurs. Copy-paste-searching any of them should land on this page.

.stream() yields one big chunk
# Expected token-by-token output; got one final chunk instead
chain = prompt | model | PydanticOutputParser(pydantic_object=MyModel)
for chunk in chain.stream({"q": "..."}):
    print(chunk)
# <Instance MyModel>   <-- only one iteration, no incremental chunks
Async stream blocks until finish
# Same problem async — no yield until the whole thing completes
async for chunk in chain.astream({"q": "..."}):
    print(chunk)
# ... several seconds of no output ... then one dump
Custom lambda destroys streaming
# The lambda materializes the whole output before returning
chain = model | (lambda ai: ai.content.upper())  # ← buffers
# Rewrite as a generator or use StrOutputParser instead

Reference

Streaming behaviour by common LangChain component

ComponentStreams natively?Notes
ChatOpenAI, ChatAnthropic, other chat modelsYesStream tokens by default
StrOutputParserYesPasses chunks through
JsonOutputParser (list root)Yes — array elementsEmits complete items as they parse
JsonOutputParser (object root)Partial — merged chunksEmits progressive partial dicts
PydanticOutputParserNo — buffers to validateWhole message needed before validation
with_structured_output()Depends on methodfunction_calling buffers; json_mode streams
Custom RunnableLambdaNo unless generatorBuffers by default
RetrieverNoRetrieval is one-shot

Root causes, ranked by frequency

Based on developer reports across LangChain forums, GitHub issues, and Discord community during 2025–2026.

  • 28%
    PydanticOutputParser at the end of the chain. Waits for the whole output to validate; nothing streams past it.
  • 18%
    Custom lambda that materializes the full input. lambda ai: ai.content.upper() consumes the whole AIMessage before returning.
  • 14%
    with_structured_output with function_calling method. Provider tool_use responses are not streamed as text.
  • 12%
    Retriever step in the middle of the chain. Not a streaming primitive; blocks until documents return.
  • 8%
    Server-side buffering. Your web framework (Flask default, some FastAPI configs) buffers responses before forwarding.
  • 8%
    Model provider or SDK not streaming. Some providers or model versions do not support streaming; the chat model falls back to non-stream.
  • 7%
    Using invoke and expecting stream. chain.invoke() never streams; you must use chain.stream() or chain.astream().
  • 5%
    Iteration consumed elsewhere. Wrapping the stream in a list comprehension defeats the purpose.

Fixes — copy-paste solutions

Fix #1

Identify the buffering step with astream_events

The events API shows exactly which step emits which chunks.

.astream_events() emits a stream of events — on_chain_start, on_llm_stream, on_parser_stream, etc. Filter events to see whether each step is streaming as expected.

diagnose_streaming.py
import asyncio
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser, PydanticOutputParser
from pydantic import BaseModel

class Answer(BaseModel):
    answer: str
    confidence: float

prompt = ChatPromptTemplate.from_template("Answer: {question}")
model = ChatOpenAI(model="gpt-4o-mini")
parser = PydanticOutputParser(pydantic_object=Answer)

# Chain A: streams ✓
chain_a = prompt | model | StrOutputParser()

# Chain B: does not stream past the parser ✗
chain_b = prompt | model | parser

async def diagnose(chain, name):
    print(f"\n=== {name} ===")
    chunk_count = 0
    async for event in chain.astream_events({"question": "hi"}, version="v2"):
        kind = event["event"]
        if kind in ("on_chat_model_stream", "on_parser_stream", "on_chain_stream"):
            data = event.get("data", {}).get("chunk", "")
            if hasattr(data, "content"):
                data = data.content
            if data:
                chunk_count += 1
                print(f"  [{event['name']:20s}] chunk: {str(data)[:40]!r}")
    print(f"  Total streaming chunks: {chunk_count}")

asyncio.run(diagnose(chain_a, "chain_a: prompt | model | StrOutputParser"))
asyncio.run(diagnose(chain_b, "chain_b: prompt | model | PydanticOutputParser"))

# chain_a: many chunks streaming from model and passing through StrOutputParser
# chain_b: chunks from model, then a single emission at the end from parser
Run this diagnostic once per chain that should be streaming. It becomes obvious where the buffering happens. Bookmark this pattern — you will use it often.
Fix #2

Replace buffering parsers with streaming-friendly alternatives

When you need structure AND streaming, JSON mode + JsonOutputParser is the way.

If the buffering step is PydanticOutputParser or with_structured_output(), switch to JSON mode with JsonOutputParser which emits progressive partial dicts as tokens arrive.

streaming_structured.py
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import JsonOutputParser
from pydantic import BaseModel, Field
from typing import List

# ❌ NON-STREAMING — PydanticOutputParser buffers
# from langchain_core.output_parsers import PydanticOutputParser
# chain = prompt | model | PydanticOutputParser(pydantic_object=Answer)

# ✓ STREAMING — JsonOutputParser emits progressive partial dicts
class Answer(BaseModel):
    steps: List[str] = Field(description="Reasoning steps")
    final: str = Field(description="Final answer")

# Use JSON mode on the model, JsonOutputParser at the end
model_json = ChatOpenAI(model="gpt-4o-mini").bind(
    response_format={"type": "json_object"}
)

parser = JsonOutputParser(pydantic_object=Answer)

prompt = ChatPromptTemplate.from_messages([
    ("system", "Answer as JSON. {format_instructions}"),
    ("human", "{question}"),
]).partial(format_instructions=parser.get_format_instructions())

chain = prompt | model_json | parser

# Now streaming yields progressively-populated dicts:
for chunk in chain.stream({"question": "Explain photosynthesis"}):
    print(chunk)
# {}
# {'steps': []}
# {'steps': ['Plants absorb']}
# {'steps': ['Plants absorb sunlight']}
# {'steps': ['Plants absorb sunlight', 'Chlorophyll captures']}
# ...
# {'steps': [...], 'final': 'Photosynthesis converts...'}
The progressive partial dicts are perfect for streaming UIs — render the partial state on each chunk. For strict Pydantic validation, apply it only to the final complete dict after the stream ends.
Fix #3

Rewrite custom lambdas as generators

A generator that yields keeps the stream flowing.

When you need a custom transformation but must preserve streaming, wrap it in RunnableGenerator or write it as a generator function.

streaming_lambda.py
from typing import Iterator
from langchain_core.runnables import RunnableGenerator
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser

model = ChatOpenAI(model="gpt-4o-mini")

# ❌ BUFFERING — full text arrives, then transformation runs
# chain = model | StrOutputParser() | (lambda s: s.upper())

# ✓ STREAMING — generator preserves per-token flow
def upper_generator(chunks: Iterator[str]) -> Iterator[str]:
    """Uppercase every chunk as it arrives."""
    for chunk in chunks:
        yield chunk.upper()

chain = model | StrOutputParser() | RunnableGenerator(upper_generator)

for chunk in chain.stream("tell me a haiku"):
    print(chunk, end="", flush=True)
# Streams uppercase chunks in real time

# For async chains, use an async generator
async def upper_agen(chunks):
    async for chunk in chunks:
        yield chunk.upper()

# Async version
achain = model | StrOutputParser() | RunnableGenerator(upper_agen)

# For running arbitrary sync transformations that ARE per-chunk:
# Just use a lambda with StrOutputParser BEFORE it, and remember every
# lambda receives ONE full accumulated input by default. For per-chunk,
# always use a generator.
The key insight: RunnableLambda receives the full input, RunnableGenerator receives an iterator of chunks. Use the right primitive for your intent.

Prevention checklist

Ship these seven safeguards once and this error stops appearing in your logs.

  • Prefer StrOutputParser and JsonOutputParser over Pydantic parsers when streaming is required.
  • Wrap custom transformations in RunnableGenerator when they must preserve streaming.
  • Use astream_events(version="v2") for diagnosing where streaming breaks.
  • For structured streaming output, use JSON mode + JsonOutputParser, not tool_calling parsers.
  • Test streaming behaviour in unit tests: assert that streaming yields more than one chunk.
  • For web apps, verify your framework (FastAPI, Flask, Node) does not buffer the response.
  • Document per-chain: "streaming friendly" or "one-shot" — the difference matters for user experience.

Frequently asked questions

Tool-calling responses from most providers are not natural text — they are structured tool_use blocks. LangChain buffers these to a complete tool call before emitting. For streaming user-facing text with tools, you need custom logic to separate tool calls from response text.
For FastAPI use StreamingResponse with an async generator that yields each chunk. Flask uses Response(generator, mimetype="text/event-stream"). Both require your web server (uvicorn, gunicorn) to not buffer — disable proxy buffering (nginx proxy_buffering off;).
Yes — RunnableParallel streams multiple branches concurrently and merges chunks. Each chunk is a partial dict with one or more branches populated. Downstream steps see this progressive-dict flow.
v2 is the current recommended version with cleaner event shapes and better filtering. v1 is legacy. Always pass version="v2" in new code — v1 will eventually be removed.
Streaming does not change the total tokens billed. It reduces perceived latency (first token appears sooner). Occasionally streaming is fractionally slower on the whole call because of extra overhead, but the UX win is dominant.

Get the weekly AI-error digest

New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.