A8.0 What This Teaches
- Server-Sent Events (SSE) - the browser standard for server-pushed data
- Flask streaming responses with generator functions
- The browser's
EventSourceAPI for reading SSE - Connecting the Anthropic streaming API to an HTTP response
- Sending a done sentinel to signal the end of the stream
A8.1 What Server-Sent Events Are
data: Hello, this is chunk one\n\n
data: And here is chunk two\n\n
data: [DONE]\n\n
\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.
EventSource object handles reconnection, buffering,
and parsing automatically. You only write event handlers.
A8.2 The Flask Streaming Route
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")
/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)
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
"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;
};
[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
http://127.0.0.1:5000, type a prompt, and click Send. Tokens
appear one by one as Claude generates them.
gunicorn --workers=1 --timeout=120 app:app
A8.8 Exercise
Exercise
Add a Stop button that cancels the stream mid-response.
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.
- Add a
<button id="stop-btn">Stop</button>next to the Send button. - In the
sendPrompt()function, store theEventSourcein a variable accessible to the Stop button's click handler. - In the Stop handler, call
es.close()and append" (stopped)"to the output div.
A8.9 Common Mistakes
Forgetting the double newline after each SSE chunk
\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
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]
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
| Term | Meaning |
|---|---|
| Server-Sent Events (SSE) | Browser standard for one-way server-to-client streaming over HTTP; each event is data: text\n\n |
| EventSource | Browser API that opens an SSE connection and fires onmessage for each event |
| Flask Response | Flask object wrapping a generator function to produce a streaming HTTP response |
| text/event-stream | MIME type that tells the browser the response is an SSE stream |
| generator function | Python function using yield; Flask calls it lazily to produce response chunks |
| sentinel | A special value (here [DONE]) that signals the end of a data stream |
| stream.text_stream | Anthropic SDK iterator that yields one string per token from a streaming API call |
| encodeURIComponent | JavaScript function that percent-encodes a string so it is safe to include in a URL |