Streaming to a Simple Frontend

Ma Mahalakshmi V Updated 16 Sep 2026
6 min read ·Lesson 63 of 224

Why Streaming Changes the User's Experience

Lesson 2's /chat endpoint waits for the model's entire response to finish generating before returning anything at all — for a short answer this is barely noticeable, but for a longer, more detailed answer grounded in retrieved document context, a user can be left staring at nothing for several seconds before any text appears. Streaming changes this by sending back each piece of the response as it's generated, so a user sees text appearing progressively rather than waiting for the complete answer — the total time to finish generating is the same either way, but the perceived responsiveness is considerably better, since the user has something to read within a fraction of a second rather than waiting for the entire answer to be ready.

Streaming a Response From the Model

The Responses API supports streaming directly, returning a sequence of events as the response is generated rather than a single completed object.

async def stream_model_response(async_client, messages: list):
    async with async_client.responses.stream(
        model="gpt-5.6-terra",
        input=messages,
    ) as stream:
        async for event in stream:
            if event.type == "response.output_text.delta":
                yield event.delta

Note: The exact streaming interface, the specific event type names, and which event carries incremental text can vary by SDK version. Confirm the current streaming API and event structure against your installed SDK version's documentation before relying on a specific event type name in production code.

Iterating over stream yields a sequence of events describing what's happening as the response is generated — checking specifically for response.output_text.delta events and yielding just the incremental text (event.delta) is what turns the full event stream, which includes other event types not relevant to display, into exactly the piece-by-piece text a frontend needs to show progressively.

Exposing a Streaming Endpoint With FastAPI

FastAPI's StreamingResponse takes an async generator and streams its output to the client as it's produced, which is exactly the shape stream_model_response() above already provides.

from fastapi import FastAPI
from fastapi.responses import StreamingResponse

app = FastAPI()

async def sse_formatted_stream(async_client, messages: list):
    async for text_chunk in stream_model_response(async_client, messages):
        yield f"data: {text_chunk}\n\n"
    yield "data: [DONE]\n\n"

@app.post("/chat/stream")
async def chat_stream(request: ChatRequest):
    history = conversation_store.setdefault(request.conversation_id, [])
    relevant_chunks = await retrieve_relevant_chunks(request.message)
    context_block = "\n\n".join(relevant_chunks) if relevant_chunks else "No relevant document content found."

    messages = [
        {"role": "system", "content": f"Answer using only this context:\n{context_block}"},
        *history,
        {"role": "user", "content": request.message},
    ]

    return StreamingResponse(
        sse_formatted_stream(async_client, messages),
        media_type="text/event-stream",
    )

sse_formatted_stream() wraps each text chunk in the Server-Sent Events (SSE) format — a data: prefix followed by two newlines — which is a simple, widely supported convention for streaming text to a browser over a regular HTTP connection, with a final [DONE] marker signaling that no more chunks are coming. Reusing Lesson 2's retrieve_relevant_chunks() and conversation-history logic unchanged here reflects that streaming only changes how the final response is delivered, not how retrieval or conversation state work — everything from Lesson 2 up through generating the messages list carries over exactly as written.

A Simple Frontend Consuming the Stream

A minimal browser-side implementation reads the streamed response incrementally and appends each chunk to the page as it arrives, rather than waiting for the entire response body.

async function sendMessage(conversationId, message) {
  const response = await fetch("/chat/stream", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ conversation_id: conversationId, message: message }),
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  const replyElement = document.getElementById("reply");
  replyElement.textContent = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    const chunkText = decoder.decode(value);
    for (const line of chunkText.split("\n\n")) {
      if (!line.startsWith("data: ")) continue;
      const data = line.slice(6);
      if (data === "[DONE]") return;
      replyElement.textContent += data;
    }
  }
}

response.body.getReader() gives direct access to the HTTP response body as it arrives, rather than waiting for fetch() to resolve with a complete body — each call to reader.read() returns whatever has arrived since the last call, which is decoded and parsed for data: lines matching the SSE format the backend produces. Appending each piece of text directly to replyElement.textContent as it's decoded is what produces the progressive, word-by-word (or chunk-by-chunk) appearance a streaming interface is meant to provide.

Handling Tool Calls Within a Streamed Response

Lesson 1's design includes a tool alongside retrieval, and a streamed response needs to account for a tool call occurring partway through generation rather than assuming every response is pure incremental text from start to finish.

async def stream_with_tool_awareness(async_client, messages: list, tools: list):
    async with async_client.responses.stream(
        model="gpt-5.6-terra",
        input=messages,
        tools=tools,
    ) as stream:
        async for event in stream:
            if event.type == "response.output_text.delta":
                yield {"type": "text", "content": event.delta}
            elif event.type == "response.function_call_arguments.delta":
                yield {"type": "tool_call_in_progress", "content": None}

Note: The exact event types emitted during a streamed response that includes a tool call, and how a completed tool call's result re-enters the stream, can vary by SDK version. Confirm the current event sequence for tool-calling streams against your installed SDK version's documentation before building production logic around a specific event type.

Distinguishing a response.function_call_arguments.delta event from a text delta matters for the frontend's experience: rather than showing nothing (or a raw, confusing partial function call) while a tool is being invoked, the frontend can show an indicator — "checking loaded documents..." — that something is happening, closing the same kind of perceived-latency gap streaming addresses for text generation, applied to the tool-calling step instead.

Common Mistakes

Buffering the entire response server-side before sending anything to the client, defeating the purpose of streaming even though the endpoint is nominally using a streaming API.

Assuming every streamed response consists only of text delta events, missing tool-call-related events entirely and producing a confusing or broken experience when the model happens to call the tool mid-response.

Not handling a dropped or interrupted connection on the frontend, leaving a user's interface stuck mid-response with no indication that the stream ended unexpectedly rather than completing normally.

Parsing the SSE stream incorrectly — such as assuming each read() call returns exactly one complete data: line — when a single chunk from the underlying connection can contain a partial line, multiple lines, or split awkwardly across chunk boundaries.

Best Practices

Stream text to the frontend incrementally as it's generated, rather than waiting for the full response, to substantially improve perceived responsiveness for longer answers.

Reuse the same retrieval and conversation-state logic between streaming and non-streaming endpoints, changing only how the final response is delivered.

Give the frontend a visible indicator for non-text events, such as an in-progress tool call, rather than leaving the interface silent while something is happening behind the scenes.

Handle stream parsing defensively, accounting for a chunk boundary that doesn't align cleanly with a complete SSE data: line.

0 Comments

Reviewed before they appear

No comments yet.

OpenAI SDK
Introduction to the OpenAI SDK Setting Up Python Creating an API Key Your First Call — client.responses.create() and response.output_text Understanding Billing, Credits, and What a Request Costs Why Responses Replaced Chat Completions Anatomy of a Request: model, input, and instructions Anatomy of a Response: The Typed output Array, Not Just Text Roles: User, Assistant, and Developer/System Choosing a Model, and Reading the Models Page Instead of Memorizing Names Instructions vs. Input Writing Prompts That Get Consistent Results Few-Shot Examples Reasoning Models and the reasoning Parameter Debugging a Prompt That Misbehaves Why Streaming Matters for User Experience stream=True and Iterating Over Events Handling the Event Types You Actually Care About Background Mode for Long-Running Jobs Project — Add Live Streaming to Your Chatbot The Problem With Parsing Free Text JSON Schema and Strict Mode Pydantic Models With the SDK's Parse Helpers Handling Refusals and Validation Failures Project — A Resume-to-JSON Extractor Working With input_image input_file, PDFs, and the Files API Image Generation Speech-to-Text and Text-to-Speech Project: A PDF Question-Answering Script What Function Calling Is Defining a Tool Schema The Full Loop Multiple Tools Errors, Timeouts, and Untrusted Arguments Project: A Weather Assistant Web Search File Search and Vector Stores Code Interpreter Remote MCP Servers and Connectors Project: A Research Assistant What an Embedding Is, Without the Maths Generating and Storing Embeddings Similarity Search From Scratch Hosted Vector Stores vs. Rolling Your Own A Small RAG App Over a Folder of Notes Agents vs. a Single API Call — When You Need One pip install openai Giving Agents Tools Handoffs and Multi-Agent Triage Guardrails and Approvals Tracing and Observing What Your Agent Did A Multi-Agent Support Desk Error Codes and What Each One Means Retries, Timeouts, and Backoff Rate Limits and Spend Limits Prompt Caching and Cost Optimisation The Batch API for Bulk Work Async Clients and Concurrency Moderation and Safety Best Practices Designing the App Backend With FastAPI Streaming to a Simple Frontend Deploying and a Cost/Safety Checklist Why Web Search Is Useful for Current Information Using the Web Search Tool with the Responses API Configuring Search Behavior for Application Use Cases Understanding Citations and Source Attribution Where to Go Next Building a Research Assistant with Web Search Combining Web Search with Structured Outputs Handling Conflicting or Low-Quality Web Sources Reducing Unsupported Claims with Grounded Generation Testing Freshness-Sensitive AI Answers Production Considerations for Web-Grounded Applications Understanding File Search and Retrieval-Augmented Generation Creating and Organizing Vector Stores Uploading Documents for Retrieval Connecting Vector Stores to Responses API Requests Designing Document Metadata and Filtering Strategies Building a PDF Question-Answering Application Improving Retrieval Quality With Better Document Preparation Handling Missing Evidence and Retrieval Failures Combining File Search With Web Search Building a Production Knowledge-Base Assistant What the Code Interpreter Tool Is Designed For Running Python-Based Analysis Through the OpenAI SDK Uploading Datasets for Analysis Analyzing CSV and Spreadsheet Data Generating Charts and Data Summaries Handling Generated Files and Downloadable Artifacts Building a Data-Analysis Assistant Combining Code Execution with Structured Outputs Validating Generated Calculations and Results Security and Sandbox Considerations for Code Execution Understanding Multimodal Input with the OpenAI SDK Sending Images to a Model Image Analysis from URLs and Uploaded Files Extracting Text and Information from Screenshots Building an Image-Question-Answering Application Combining Image Input with Structured Output Analyzing Multiple Images in One Request Handling Image Quality and Input Limitations Designing Multimodal Prompts for Reliable Results Building a Practical Vision-Powered Python Application Understanding Speech-to-Text and Text-to-Speech Workflows Transcribing Audio with the OpenAI SDK Working with Uploaded Audio Files Handling Timestamps and Transcription Metadata Building a Meeting Transcription Workflow Generating Spoken Responses from Text Handling Long Audio and Processing Failures Combining Audio with Text and Tool Calling Building an End-to-End Python Voice Application What Embeddings Are and When to Use Them Generating Embeddings With the OpenAI API Preparing Text for Embedding Comparing Vectors With Cosine Similarity Building a Simple Semantic Search Engine in Python Storing Embeddings in a Database Metadata Filtering for Semantic Search Chunking Strategies for Better Retrieval Evaluating Semantic Search Quality Building a Document Similarity Application When Batch Processing Makes Sense Designing Large-Volume AI Processing Pipelines Using Asynchronous Python with the OpenAI SDK Running Concurrent Requests Safely Controlling Concurrency and Avoiding Rate Limits Tracking Batch Job Progress Handling Partial Failures in Bulk Workloads Retrying Failed Items Without Duplicating Successful Work Designing Resumable AI Processing Jobs Building a Production Batch-Processing Pipeline Batch Processing Makes Sense Large-Scale AI Processing Pipelines Async Python with OpenAI SDK Safe Concurrent Requests Concurrency & Rate Limits Batch Progress Tracking Partial Failure Handling Safe Retry Handling Resumable AI Jobs Production Batch Pipeline System–User Data Separation Reusable App Instructions Prompt Templates & Variables Extraction & Classification Prompts Summarization & Transformation Prompts Explicit Output Requirements Prompt Version Management Prompt Testing & Evaluation Reusable Python Prompt Library API Key Security Secure API Key Storage Secure Secret Management Prompt Injection Prevention Trusted vs. Untrusted Content Tool Argument Validation Sensitive Data Handling Secure Logging AI Action Authorization Production AI Security Checklist Why AI Applications Need Evaluation Beyond Unit Tests Unit Testing OpenAI SDK Integration Code Mocking API Responses in Python Tests Testing Structured Outputs Against Schemas Testing Tool-Calling Workflows Building a Small Evaluation Dataset Measuring Accuracy, Consistency, and Failure Rates Regression Testing Prompts and Model Changes Human Evaluation Versus Automated Evaluation Creating a Repeatable Evaluation Pipeline AI Request Monitoring Token Cost Management Usage Metrics Design Reducing Model Calls Prompt & Context Optimization Model Selection & Optimization AI Caching Strategies Interactive Latency Optimization Usage Dashboards & Budget Alerts Performance & Cost Checklist Every API Call Starts Fresh Fixing API Statelessness Server-Side Conversation Memory Limits of Response Chaining What We're Building Conversation Memory Challenges Preparing an OpenAI SDK Application for Deployment Environment-Specific Configuration for Development and Production Deploying a Python AI Service with Docker Container Health Checks and Startup Configuration Managing Secrets in Cloud Deployments Background Workers for Long-Running AI Tasks Queues and Asynchronous Job Architectures Scaling AI Workloads Horizontally Monitoring Production Incidents and Failures Production Deployment Checklist for OpenAI SDK Applications Reusable OpenAI Service Classes AI Client Dependency Injection Typed AI Responses Python Configuration Management AI Request Decorators Centralized AI Error Handling Clean SDK Abstractions Reusable OpenAI Utilities Internal AI Python Libraries SDK Integration Maintenance Production AI Chatbot Document Q&A System Web Research Assistant Customer Support Agent AI Data Analysis Assistant Image Analysis App Meeting Transcription & Summary Semantic Document Search Multi-Tool AI Agent Production OpenAI SDK App Why "It Looked Fine When I Tested It" Isn't Enough Timing Note Status Note Pre-Decision Status Note Current Availability Note
Ask about this post
AI Ask about this post

Ask questions about Streaming to a Simple Frontend and get answers drawn from it.

Signed-in readers only.