Enterprise DNA

Omni by Enterprise DNA

Enterprise DNA Resources

Step-by-step how-tos. Practical AI operating-system thinking for owners, operators, and teams doing real work.

220k+

Data professionals

Omni

AI agents and apps

Audit

Map the manual work

Guide Intermediate General

LLM Streaming Responses in Python: A Practical Guide

A working walkthrough of streaming LLM responses in Python with the OpenAI SDK, covering setup, code, settings, and production patterns.

Sam McKay |
LLM Streaming Responses in Python: A Practical Guide

What Streaming Responses Actually Is

Strip away the marketing and streaming responses are just chunked HTTP transfers. When you call an LLM with streaming enabled, the server does not wait to compose the full answer before sending anything. It pushes tokens out the wire the moment they are generated, wrapped in Server-Sent Events format, and your client receives them as they arrive.

The wire format looks like this. Each line starts with data: , followed by a JSON object containing a delta instead of a full message. The deltas contain just the new characters or tokens produced since the last chunk. When the model finishes, the server sends a final data: [DONE] line and closes the stream.

Three things matter for your mental model. First, you are not getting a faster model. The total time to generate a full response is the same as non-streaming. What changes is the time to first token, which drops from “wait for everything” to “wait for the first character”. Second, each chunk is a delta, not a snapshot, so you have to accumulate them yourself if you want the full text. Third, usage statistics (token counts) are only sent at the end, never in the chunks, which breaks any naive cost-tracking pattern you might copy from a non-streaming example.

The practical consequence is that streaming is a UX optimization, not a cost or performance optimization. You use it when a human is watching the screen and would notice a 3 to 8 second blank pause. You avoid it when the downstream consumer is a script that needs the whole thing to do anything useful.

Setup and Authentication

For the OpenAI Python SDK, the setup is a single pip install and a single environment variable.

pip install openai

Then export your key.

export OPENAI_API_KEY="sk-..."

You can confirm the install is wired up with a one-liner that lists the available models, but the first streaming call itself is the real test. If the key is valid and the network is open, you will get a response in under a second. If not, you will get a 401 or a network error, not a silent hang.

For Anthropic, the package is anthropic and the environment variable is ANTHROPIC_API_KEY. The streaming pattern is structurally identical. For local models served through Ollama, the OpenAI-compatible endpoint at http://localhost:11434/v1 works with the same client, which is useful if you want one code path for both cloud and local inference.

A detail that bites people: load the key from the environment, not from a config file checked into git. Use python-dotenv for local development and read the variable directly in production. The SDK will pick up OPENAI_API_KEY automatically, so you can skip passing it explicitly to the client.

First Working Example

Here is a minimal, runnable script that streams a chat completion and prints tokens as they arrive.

from openai import OpenAI

client = OpenAI()

stream = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Explain Server-Sent Events in two sentences."}],
    stream=True,
)

full = []
for chunk in stream:
    delta = chunk.choices[0].delta
    if delta.content is not None:
        print(delta.content, end="", flush=True)
        full.append(delta.content)

print()
complete = "".join(full)

Three things to notice. The stream=True flag is the only thing that flips the request into streaming mode. Everything else is identical to a normal call. The delta object on each chunk may have content of None for the first chunk (when the role is being established) and for the final chunk (which carries the finish reason). You have to guard for that, or you will get a TypeError on the print line. Finally, the accumulating full list and the complete string let you recover the full response after the stream ends, which is what you would hand to the next stage of your pipeline.

Run it. The output should appear word by word instead of all at once. The total wall-clock time will be the same as a non-streaming call, but the first word will hit your terminal in roughly 200 to 500 milliseconds.

Key Settings That Matter

The stream=True flag gets all the attention, but the surrounding parameters do most of the actual work.

stream_options={"include_usage": True} is the one most people miss. By default, streaming responses do not include token usage in the final chunk, which means your logging and cost tracking break. Passing this option restores the usage object on the last chunk. If you are running this in production and care about cost, you need it.

max_tokens caps the response length. With streaming, this becomes a real safety valve. A runaway generation that would have produced 10,000 tokens in non-streaming mode will produce 10,000 streamed chunks before it stops, and your buffer will balloon. Setting max_tokens to a sensible upper bound keeps the stream bounded.

temperature behaves identically to non-streaming but interacts with chunking in subtle ways. At temperature zero, the first token arrives at a near-fixed latency. At temperature 1.5 with a long prompt, the variance on the first token can swing from 300 milliseconds to over 2 seconds. If you need predictable latency, pin temperature low.

timeout on the client is the silent killer. The default in the Python SDK is 600 seconds, which is fine for one request but awful for a streamed request that is hung. Set it explicitly. For chat workloads, 30 to 60 seconds is typical for APIs at this tier. For reasoning models that think before responding, double it.

max_retries controls how the SDK handles transient failures on streaming responses. The default is 2. Streaming retries are tricky because the first attempt may have already started returning chunks, and a retry starts from scratch. For production, set this to 0 and handle retries yourself with idempotency keys, or accept that long streams may fail partway.

stop sequences are critical for streaming because they let you end generation early when a known pattern appears. If you are generating structured output and the model can occasionally produce a closing delimiter twice, the second appearance will fire the stop and you will get a clean end. Without it, you will see the duplicate in your output and have to filter it downstream.

Reasoning models stream their internal thinking as part of the response. The thinking tokens arrive first, then the final answer. If you are only interested in the final answer, you have to filter the stream and discard reasoning content. This is a real consideration for latency budgets because the reasoning phase can take 10 to 60 seconds before the first user-facing token appears.

Where It Shines

Chat interfaces are the obvious one. Anything that mimics the feel of a human typing benefits from streaming, and the perceived performance gain is real even when the total time is identical. Most modern chat UIs are doing exactly this.

Code generation is a close second. When a model is producing a 200-line function, watching it stream is far more useful than waiting 12 seconds for the full block. You can spot a wrong direction early and interrupt, which matters when you are paying per token.

Long-form content, like article drafting, document summarization, and report generation, is the third strong case. The progressive disclosure reduces the cognitive load of “is this thing still working” anxiety that comes with long non-streaming waits.

Interactive applications, like tutoring tools, code review bots, and live assistants, fall into the same bucket. Anywhere a human is waiting on the response and the response is long enough that the wait is noticeable (over about 1.5 seconds), streaming helps.

Where It Fails

Structured output is the first place streaming fights you. If you are asking the model to produce JSON, the JSON is invalid until the closing brace arrives, which means you cannot parse it mid-stream. Some libraries have started supporting partial parsing, but for most use cases you still need the full response. Streaming buys you nothing here except complexity.

Batch processing is the opposite case. If you are processing 10,000 prompts overnight, streaming does not help and adds overhead. Use the regular create call and let the server batch internally. You will