Text size
Dark mode
RecallRunGuides & benchmarks
Learn to build real systemsGuides•Benchmarks•Experiments•Community
Search the site/Free checklist

Guides/OpenAI SDK

OpenAI SDK: Streaming responses

A model generates output one token at a time. Without streaming you wait for the whole answer, perhaps 8 seconds for a long reply, and see nothing meanwhile.

In this post

What this post covers
  1. 1. Same total time, very different experience
  2. 2. stream=True: iterate over events
  3. 3. The helper: client.responses.stream()
  4. 4. Streaming to a browser
  5. Recap

A model generates output one token at a time. Without streaming you wait for the whole answer, perhaps 8 seconds for a long reply, and see nothing meanwhile. With streaming, the server sends each piece as soon as it exists (as server-sent events), so the user sees text after a few hundred milliseconds. Total time is the same, but time to first token is what users feel.

1. Same total time, very different experience

No streaming blank screen… (8 s) whole answer Streaming words appear continuously from ~0.4 s → done at 8 s ↑ time to first token (TTFT): the number that decides whether an app "feels fast"

2. stream=True: iterate over events

from fake_openai import fake_client

client = fake_client(script=["Kochi, Bengaluru and Pune had the highest revenue in October."])
stream = client.responses.create(model="gpt-5.6-luna", input="Top 3 cities by revenue?", stream=True)

for event in stream:
    if event.type == "response.output_text.delta":
        print(event.delta, end="", flush=True)          # print each piece as it arrives
    elif event.type == "response.completed":
        usage = event.response.usage
        print(f"\n[done: {usage.input_tokens} in / {usage.output_tokens} out]")

Output

Kochi, Bengaluru and Pune had the highest revenue in October.
[done: 6 in / 15 out]

all the event types in one stream

from collections import Counter
from fake_openai import fake_client

client = fake_client(script=["Streaming is just a sequence of typed events."])
events = list(client.responses.create(model="gpt-5.6-luna", input="Explain streaming", stream=True))
print(Counter(e.type for e in events))
print([e.sequence_number for e in events][:5], "…")

Output

Counter({'response.output_text.delta': 8, 'response.created': 1, 'response.output_item.added': 1, 'response.content_part.added': 1, 'response.output_text.done': 1, 'response.content_part.done': 1, 'response.output_item.done': 1, 'response.completed': 1})
[0, 1, 2, 3, 4] …

A stream is a sequence of typed events: the response is created, an output item is added, text deltas arrive, items finish, and the response completes (or fails, with an error or response.failed event). Tool calls stream too, as argument deltas. Handle only the event types you need and ignore the rest, so new event types never break your code.

3. The helper: client.responses.stream()

from fake_openai import fake_client

client = fake_client(script=["The helper accumulates the full response for you."])
with client.responses.stream(model="gpt-5.6-luna", input="Why use the helper?") as stream:
    for event in stream:
        if event.type == "response.output_text.delta":
            print(event.delta, end="")
    final = stream.get_final_response()          # a normal Response object, as if not streamed
print("\nfinal.output_text:", final.output_text)

Output

The helper accumulates the full response for you.
final.output_text: The helper accumulates the full response for you.

The context manager closes the connection even if your loop breaks early, and get_final_response() gives you the assembled Response for logging or saving. It also supports text_format= for streamed structured output (part 06).

4. Streaming to a browser

FastAPI endpoint that relays tokens to the browser (sketch)

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from openai import AsyncOpenAI

app = FastAPI()
client = AsyncOpenAI()

@app.get("/ask")
async def ask(q: str):
    async def tokens():
        stream = await client.responses.create(model="gpt-5.6-luna", input=q, stream=True)
        async for event in stream:
            if event.type == "response.output_text.delta":
                yield event.delta
    return StreamingResponse(tokens(), media_type="text/plain")
Things to get right

Use the async client in async web servers (part 11). Handle the user disconnecting: stop iterating so you stop paying for tokens nobody reads. Moderate or validate the full text after the stream ends, because a partial stream has already been shown (part 16). And log usage from the completed event.

Recap

  • Streaming improves time to first token, not total time.
  • stream=True yields typed events; use response.output_text.delta for text and response.completed for usage.
  • responses.stream() manages the connection and assembles the final response.
  • In web apps: async client, handle disconnects, validate the full text at the end.

Get the free RAG production checklist

25 checks to run before real users see your RAG feature: exact-match misses, invented citations, prompt injection through documents, quality that slips after a prompt change. Confirm your email and the PDF comes with the welcome email, plus each new experiment.

One email to confirm. Unsubscribe any time. What's in it · Privacy

Want the production version of this? See ShipRAG: A production RAG + MCP starter kit for FastAPI: hybrid search, validated citations, evals in CI, Docker, 32 tests.

Written by

Alby Tomy

Python/FastAPI Engineer | Data Engineer | AI Engineer building RAG and MCP systems.

X · GitHub · Contact & enquiries