Streaming Responses

RoadmapsAI Engineering

Scenario

Your LLM pipeline takes 8 seconds to generate a 400-token answer. Users see a blank screen for 8 seconds, then the full text appears at once. Before you read on, predict: does streaming make the model generate faster, or does something else change?

Think about the difference between generation time and perceived wait — then read on.

Mental model

Streaming is the API sending you tokens as they are generated, instead of waiting to wrap everything up first.

A language model generates text token by token. Without streaming, the API buffers all tokens and sends one big response when generation completes — you wait the full 8 seconds. With `stream=True`, the API opens a long-lived HTTP connection and sends each token the moment it is ready. Your app can render it immediately. Total generation time is identical, but users start reading in about 200 milliseconds.

Deep dive

Without streaming: the model generates 400 tokens over 8 seconds; the API buffers everything; you wait 8 seconds; the full text arrives at once. With streaming: the API sends a Server-Sent Events (SSE) stream; you receive tokens as they arrive — the first token might reach you in 200ms. Total generation time is the same, but users start reading immediately.

Streaming = same compute time, tokens arrive progressively. First token in ~200ms instead of 8s.

Each SSE chunk is a delta — not the full text so far, just the new piece. You accumulate them into a growing string. The stream ends when a chunk's `finish_reason` is `'stop'` (or `'length'` or `'tool_calls'`). Until then `finish_reason` is `None`. The token usage count (tokens billed) is only available on the LAST chunk — you cannot count tokens mid-stream without using the `tiktoken` library.

Chunks are deltas. Accumulate them. finish_reason and usage are on the last chunk only.

**When NOT to stream**: if your pipeline needs the complete answer to decide what to do next — parse JSON, route based on classification, extract citations — streaming adds complexity with no benefit. You would still need to wait for the full text before acting. Stream only for user-facing text output where the typing effect improves perceived responsiveness.

Stream for user-facing output. Skip streaming for pipeline steps that parse the full response.

Code examples

Basic streaming — print tokens as they arrive

from openai import OpenAI
client = OpenAI()

stream = client.chat.completions.create(
    model='gpt-4o',
    messages=[{'role': 'user', 'content': 'Explain gradient descent in simple terms.'}],
    max_tokens=300,
    stream=True,    # <-- the only change from a normal call
)

for chunk in stream:
    delta = chunk.choices[0].delta.content
    if delta:                       # last chunk has delta=None
        print(delta, end='', flush=True)

print()  # newline after stream ends

`stream=True` turns the return value from a response object into an iterator. `delta.content` is the new text piece — it is `None` on the last chunk, which carries `finish_reason`. The `flush=True` makes tokens appear immediately in the terminal instead of buffering.

Accumulate the full text and get token usage

from openai import OpenAI
client = OpenAI()

stream = client.chat.completions.create(
    model='gpt-4o',
    messages=[{'role': 'user', 'content': 'List 5 Python best practices.'}],
    max_tokens=400,
    stream=True,
    stream_options={'include_usage': True},  # request usage in final chunk
)

full_text = []
for chunk in stream:
    delta = chunk.choices[0].delta.content
    if delta:
        full_text.append(delta)
        print(delta, end='', flush=True)  # stream to user
    if chunk.usage:                        # last chunk only
        print(f'\nTotal tokens: {chunk.usage.total_tokens}')

complete = ''.join(full_text)  # parse or store the complete answer here

`stream_options={'include_usage': True}` appends a final chunk with token counts — otherwise `usage` is `None` for all chunks. This is the production pattern: stream to the user AND log costs by collecting the final `usage` object.

Common mistakes

Glossary

streaming
A mode where the API sends the AI's answer to you piece by piece as it is generated, instead of waiting for the full answer to be ready first.
Server-Sent Events
The underlying web technology that keeps a connection open so the server can push new text fragments to your application in real time.
delta
A small fragment of new text that arrives in one stream chunk. You accumulate all deltas together to get the full response.

Recall questions

Questions & answers

You enable streaming for your RAG pipeline's answer-generation step. Afterwards, your citation-extraction code fails intermittently. Why?

Citation extraction reads the full text to find patterns like [1] and [2]. With streaming, the code tries to parse the response before the full text has accumulated — the text is incomplete and the parser fails or finds partial citations. Fix: accumulate the full streamed text first, then pass it to the citation extractor. Or disable streaming for this pipeline step entirely.

Approach: Identify the streaming-vs-parsing mismatch. Streaming is for user-facing output; programmatic parsing needs the complete string.

A user sees the streaming output stop mid-sentence with no error shown. What are two likely causes?

First: `max_tokens` was hit — `finish_reason='length'` — the model was truncated before finishing. Fix: increase `max_tokens` or shorten the prompt. Second: a network timeout dropped the SSE connection mid-stream. Fix: implement reconnect logic or catch the stream interruption and retry from scratch. Check `finish_reason` on the last chunk to distinguish between the two.

Approach: Two root causes: model-side truncation (finish_reason='length') vs network-side connection drop. They look the same to the user but need different fixes.

Continue learning

Previous: Chat Completions API

Return to AI Engineering Roadmap