Site

App: Web Streaming — SSE Responses

Tutorial A8.0  •  AI / Learn / Applications

A8.0 What This Teaches

This tutorial connects Claude's streaming API to a browser using Server-Sent Events, so tokens appear in the page as they are generated rather than all at once. It covers:

A8.1 What Server-Sent Events Are

SSE is a browser standard for receiving a continuous stream of data over a single HTTP connection. The browser opens the connection once; the server sends chunks as they become available. Each chunk uses a simple text format:
data: Hello, this is chunk one\n\n
data: And here is chunk two\n\n
data: [DONE]\n\n
The double newline (\n\n) marks the end of each event. A single \n is not enough - the browser will not fire the onmessage callback until it sees \n\n. The browser's EventSource object handles reconnection, buffering, and parsing automatically. You only write event handlers.

A8.2 The Flask Streaming Route

A Flask route returns a streaming response by passing a generator function to Response() with mimetype="text/event-stream". The generator yields SSE-formatted strings as Claude produces tokens.
# Flask streaming route pattern.
from flask import Response

def generate(prompt):
    # each yield pushes one SSE event to the browser
    with client.messages.stream(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        messages=[{"role": "user", "content": prompt}],
    ) as stream:
        for chunk in stream.text_stream:
            yield f"data: {chunk}\n\n"
    yield "data: [DONE]\n\n"   # sentinel tells the browser to close

@app.route("/stream")
def stream():
    prompt = request.args.get("prompt", "")
    return Response(generate(prompt), mimetype="text/event-stream")
The /stream route uses GET with a query parameter because EventSource only supports GET requests.

A8.3 Complete app.py

# app.py - Flask server with SSE streaming from Claude.
# Run: python app.py, then open http://127.0.0.1:5000

from flask import Flask, render_template, request, Response
import anthropic

app = Flask(__name__)
client = anthropic.Anthropic()

def generate(prompt):
    # stream.text_stream yields one string per token
    with client.messages.stream(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        messages=[{"role": "user", "content": prompt}],
    ) as stream:
        for chunk in stream.text_stream:
            # escape newlines inside chunk so SSE format stays intact
            safe = chunk.replace("\n", " ")
            yield f"data: {safe}\n\n"
    yield "data: [DONE]\n\n"

@app.route("/")
def index():
    return render_template("index.html")

@app.route("/stream")
def stream():
    prompt = request.args.get("prompt", "").strip()
    if not prompt:
        return Response("data: [DONE]\n\n", mimetype="text/event-stream")
    return Response(generate(prompt), mimetype="text/event-stream")

if __name__ == "__main__":
    app.run(debug=True)
Newlines inside a chunk are replaced with spaces so each data: line stays on one line. The browser reconstructs the full text from consecutive chunks.

A8.4 The Browser JavaScript

EventSource opens the SSE connection. Each arriving event fires onmessage. Check the data for the [DONE] sentinel and close the connection when it arrives.
// Opens an SSE connection and streams tokens into the output div.
function sendPrompt() {
  const prompt = document.getElementById('prompt-input').value.trim();
  if (!prompt) return;

  const output = document.getElementById('output');
  output.textContent = '';   // clear previous response

  const es = new EventSource('/stream?prompt=' + encodeURIComponent(prompt));

  es.onmessage = function(e) {
    if (e.data === '[DONE]') {
      es.close();
      return;
    }
    output.textContent += e.data;
  };

  es.onerror = function() {
    es.close();
    output.textContent += '\n[stream error]';
  };
}

A8.5 Sending a Done Sentinel

Without a sentinel, the browser keeps the SSE connection open indefinitely waiting for more events. The generator yields "data: [DONE]\n\n" after the stream loop ends, and the JavaScript closes the EventSource when it sees that value.
# Sentinel pattern - last thing the generator yields.
yield "data: [DONE]\n\n"
// Browser side - close on sentinel.
es.onmessage = function(e) {
  if (e.data === '[DONE]') { es.close(); return; }
  output.textContent += e.data;
};
Choose a sentinel value that Claude will never produce as real output. [DONE] is a common convention borrowed from the OpenAI streaming format.

A8.6 Complete index.html

<!-- templates/index.html - SSE streaming chat UI -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Streaming Chat</title>
  <style>
    body { font-family: sans-serif; max-width: 700px; margin: 2rem auto; }
    #output { border: 1px solid #ccc; min-height: 200px; padding: 1rem;
              white-space: pre-wrap; margin-top: 1rem; }
    #controls { display: flex; gap: 0.5rem; margin-top: 1rem; }
    #prompt-input { flex: 1; padding: 0.4rem; }
  </style>
</head>
<body>
  <h1>Streaming Chat</h1>
  <div id="controls">
    <input id="prompt-input" type="text" placeholder="Ask something...">
    <button onclick="sendPrompt()">Send</button>
  </div>
  <div id="output"></div>

  <script>
    let activeEs = null;

    function sendPrompt() {
      if (activeEs) { activeEs.close(); activeEs = null; }

      const prompt = document.getElementById('prompt-input').value.trim();
      if (!prompt) return;

      const output = document.getElementById('output');
      output.textContent = '';

      activeEs = new EventSource('/stream?prompt=' + encodeURIComponent(prompt));

      activeEs.onmessage = function(e) {
        if (e.data === '[DONE]') { activeEs.close(); activeEs = null; return; }
        output.textContent += e.data;
      };

      activeEs.onerror = function() {
        activeEs.close();
        activeEs = null;
        output.textContent += '\n[stream error]';
      };
    }
  </script>
</body>
</html>

A8.7 Running the Application

pip install flask anthropic
cd web_streaming
python app.py
Open http://127.0.0.1:5000, type a prompt, and click Send. Tokens appear one by one as Claude generates them. Flask's built-in dev server supports streaming. For production, use gunicorn with a single worker or an ASGI server like uvicorn:
gunicorn --workers=1 --timeout=120 app:app
Multiple workers break SSE because each request may hit a different worker process. A single worker or a proper async server (uvicorn + FastAPI) avoids this.

A8.8 Exercise

Exercise Add a Stop button that cancels the stream mid-response.
  1. Add a <button id="stop-btn">Stop</button> next to the Send button.
  2. In the sendPrompt() function, store the EventSource in a variable accessible to the Stop button's click handler.
  3. In the Stop handler, call es.close() and append " (stopped)" to the output div.
Test it: send a long prompt like "Write a 500-word essay about the ocean", click Stop after a few sentences, and confirm the stream halts.

A8.9 Common Mistakes

Forgetting the double newline after each SSE chunk

SSE requires \n\n (two newlines) to terminate each event. Using a single \n means the browser buffers the data and never fires onmessage. The stream appears broken even though the server is sending data correctly.

Using POST for the streaming route

The browser's EventSource API only supports GET requests. Attempting to point it at a POST route results in a connection error. Pass the prompt as a URL query parameter with encodeURIComponent() to handle special characters.

Not closing the EventSource on [DONE]

If the browser never calls es.close(), the connection stays open and the browser periodically retries it. This produces duplicate responses and consumes server resources. Always close the EventSource when the sentinel arrives.

Key Terms

TermMeaning
Server-Sent Events (SSE)Browser standard for one-way server-to-client streaming over HTTP; each event is data: text\n\n
EventSourceBrowser API that opens an SSE connection and fires onmessage for each event
Flask ResponseFlask object wrapping a generator function to produce a streaming HTTP response
text/event-streamMIME type that tells the browser the response is an SSE stream
generator functionPython function using yield; Flask calls it lazily to produce response chunks
sentinelA special value (here [DONE]) that signals the end of a data stream
stream.text_streamAnthropic SDK iterator that yields one string per token from a streaming API call
encodeURIComponentJavaScript function that percent-encodes a string so it is safe to include in a URL