How to Stream an AI Chat Response in Flask

If your AI chat endpoint freezes for a few seconds and then dumps the whole reply at once, the backend is buffering - collecting every token before it flushes. Pass stream=True to the LLM and yield each token immediately via SSE, and the UI types the answer out live.

Fullstack Engineerfullstackstreamingsse

Why it buffers

When you call an LLM API without stream=True, the request blocks until the model finishes generating, then returns the entire response in one payload. Your Flask endpoint reads that single payload and sends it as one HTTP response body - which is why the browser freezes, then gets the full text all at once.

Even if you enable streaming on the LLM side, forgetting to forward each chunk immediately (for example, by accumulating tokens into a string first) has the same effect - the buffering just moves from the LLM layer into your own code.

The fix: stream=True + yield per token

Use requests with stream=True, iterate r.iter_lines(), and yield each SSE chunk the moment it arrives:

import json
import requests
from flask import Flask, request, Response, stream_with_context

app = Flask(__name__)
LLM_URL = "http://llm-service:8080/v1/chat/completions"

@app.post("/api/chat/stream")
def chat_stream():
    message = (request.get_json(silent=True) or {}).get("message", "")

    @stream_with_context
    def gen():
        body = {
            "model": "your-model",
            "messages": [{"role": "user", "content": message}],
            "stream": True,
        }
        with requests.post(LLM_URL, json=body, stream=True, timeout=60) as r:
            for line in r.iter_lines():
                if not line:
                    continue
                decoded = line.decode("utf-8")
                if not decoded.startswith("data: "):
                    continue
                payload = decoded[6:]
                if payload.strip() == "[DONE]":
                    continue
                try:
                    token = json.loads(payload)["choices"][0]["delta"].get("content", "")
                except Exception:
                    continue
                if token:
                    yield f"data: {json.dumps({'content': token})}\n\n"
        yield "data: [DONE]\n\n"

    return Response(gen(), mimetype="text/event-stream")

The key points: - stream=True on the requests.post call keeps the connection open and lets iter_lines() yield each SSE line as it arrives. - @stream_with_context keeps the Flask request context alive for the duration of the generator so request.* access inside gen() is safe. - Yield immediately - do not accumulate tokens into a buffer and flush at the end. - Return mimetype="text/event-stream" so browsers and React's EventSource (or a fetch + ReadableStream reader) interpret each data: line as a chunk.

Reading the stream on the React side

const res = await fetch('/api/chat/stream', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ message }),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });
  for (const line of buffer.split('\n')) {
    if (line.startsWith('data: ') && line !== 'data: [DONE]') {
      const { content } = JSON.parse(line.slice(6));
      appendToChat(content);
    }
  }
  buffer = '';
}

Watch for buffering at every hop

Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.

Try it in a workspace →

What you'll practice

FAQ

Why does my Flask AI chat endpoint dump the whole reply at once?

The backend is buffering - either by calling the LLM without stream=True (which waits for the full reply), or by accumulating all tokens before yielding. Pass stream=True to the LLM client, iterate the response line by line, and yield each SSE chunk immediately.

What is stream_with_context in Flask?

Flask tears down the request context when the view function returns. If your generator runs after the function returns (which it does in a streaming response), request.* calls fail. @stream_with_context keeps the context alive for the lifetime of the generator.

Why does streaming work in curl but not in the browser?

A reverse proxy such as nginx is likely buffering the response before it reaches the browser. Add proxy_buffering off; to the nginx location block for the streaming endpoint.

Keep learning

Fix a React useState Stale-State BugFullstack projectFix a Blank React Dashboard (Failed Fetch)Fullstack projectFix the React Search That Won't Filter (useEffect Deps)Fullstack projectFullstack roadmapStep by step to hiredFullstack interview questionsSTAR answersAll Fullstack projectsProjects hub

Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.

Open the workspace →