Server‑Sent Events (SSE) give web developers a simple, efficient way to push real‑time updates from a Python backend directly to a browser without the overhead of WebSockets. In this guide we’ll explore how to build a robust Python SSE web app, why SSE can be the perfect fit for live dashboards, notifications, and streaming data, and we’ll walk through a complete example using Flask and FastAPI. By the end of this post you’ll have a production‑ready template that you can adapt to any project that needs low‑latency, one‑way server communication.
What Are Server‑Sent Events?
Server‑Sent Events are part of the HTML5 EventSource API. Unlike WebSockets, which establish a full‑duplex channel, SSE creates a unidirectional stream from server to client. The browser automatically reconnects if the connection drops, and the data is sent as plain text following a simple line‑based format.
- Lightweight: No binary framing, just UTF‑8 text.
- Built‑in reconnection: The client retries automatically with an exponential back‑off.
- Easy to implement: A single HTTP endpoint that returns
text/event-stream. - SEO‑friendly: Since the page itself is still served via normal HTTP, search engines can crawl the static content while the dynamic part runs in the background.
When to Choose SSE Over WebSockets
Both SSE and WebSockets can deliver real‑time data, but they excel in different scenarios. Use SSE when you need:
- Simple, one‑way updates (e.g., live scores, stock tickers, chat notifications).
- Broad browser support without polyfills (all modern browsers support
EventSource). - Low server resource consumption – SSE works over standard HTTP/1.1 or HTTP/2.
- Automatic reconnection and event ID tracking out of the box.
If you need bidirectional communication, binary data, or sub‑millisecond latency, WebSockets remain the better choice.
Setting Up the Python Environment
Required Packages
For this tutorial we’ll use two popular Python web frameworks. You can pick one based on your existing stack:
Flask– lightweight, easy to get started.FastAPI– async‑first, built on Starlette, excellent for high‑concurrency.
Install the dependencies with pip:
pip install flask fastapi uvicorn
Building an SSE Endpoint with Flask
Basic Flask App
Below is a minimal Flask app that streams the current server time every second.
from flask import Flask, Response, stream_with_context
import time
app = Flask(__name__)
def event_stream():
"""Generator that yields a new SSE message every second."""
counter = 0
while True:
counter += 1
data = f"data: Server time {time.strftime('%Y-%m-%d %H:%M:%S')} (tick {counter})\n\n"
yield data
time.sleep(1)
@app.route('/stream')
def stream():
# Set the correct MIME type for SSE
return Response(stream_with_context(event_stream()),
mimetype='text/event-stream')
if __name__ == '__main__':
app.run(debug=True, threaded=True)
Key points:
- Use
stream_with_contextto keep Flask’s request context alive. - Yield strings that follow the SSE format:
data: <payload>\n\n. - Set
mimetype='text/event-stream'so the browser knows to treat the response as an event stream.
Client‑Side JavaScript
On the front‑end, the EventSource object handles the connection automatically.
<script>
const source = new EventSource('/stream');
source.onmessage = function(event) {
console.log('Received:', event.data);
const log = document.getElementById('log');
const entry = document.createElement('div');
entry.textContent = event.data;
log.prepend(entry);
};
source.onerror = function(err) {
console.error('SSE error:', err);
};
</script>
<div id="log"></div>
Building an SSE Endpoint with FastAPI
Why FastAPI?
FastAPI leverages Python’s asyncio library, allowing thousands of concurrent connections with minimal overhead. Its built‑in support for StreamingResponse makes SSE implementation straightforward.
Async SSE Example
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio
import datetime
app = FastAPI()
async def event_generator():
"""Async generator that yields a timestamp every second."""
counter = 0
while True:
counter += 1
now = datetime.datetime.utcnow().isoformat()
# SSE format: data: ...\n\n
yield f"data: {{\"time\": \"{now}\", \"tick\": {counter}}}\n\n"
await asyncio.sleep(1)
@app.get("/sse")
async def sse_endpoint():
return StreamingResponse(event_generator(),
media_type="text/event-stream")
FastAPI automatically runs on an ASGI server (e.g., uvicorn). Start it with:
uvicorn myapp:app --reload
Client Code for FastAPI SSE
The JavaScript remains identical because the browser only cares about the text/event-stream MIME type.
<script>
const source = new EventSource('/sse');
source.addEventListener('message', (e) => {
const data = JSON.parse(e.data);
console.log('Server time:', data.time, 'Tick:', data.tick);
});
</script>
Best Practices for Production‑Ready SSE
1. Use a Reverse Proxy That Supports Streaming
- Nginx: Set
proxy_buffering off;andproxy_cache off;for the SSE location. - Apache: Enable
mod_proxy_wstunneland disable buffering withProxyPass ... ws://(even though it’s not WebSocket, the same directives apply).
2. Keep Connections Alive
Browsers may close idle connections after 30‑60 seconds. Send a comment line (:) as a heartbeat:
def event_stream():
while True:
yield ": keep‑alive\\n\\n" # comment line, ignored by client
# then your actual data...
3. Set Appropriate CORS Headers
If your front‑end lives on a different domain, add CORS support:
# Flask example
from flask_cors import CORS
CORS(app)
# FastAPI example
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Adjust for security
allow_methods=["GET"],
)
4. Limit Message Size
Browsers impose a default limit (~64 KB). If you need larger payloads, split them into multiple events or switch to WebSockets.
5. Graceful Shutdown
When the server stops, close all generator loops cleanly to avoid “broken pipe” warnings.
try:
while running:
yield data
except GeneratorExit:
# Cleanup logic here
pass
Real‑World Use Cases for Python SSE
- Live dashboards: Stream sensor data, KPI metrics, or log tails directly into a web UI.
- Chat notifications: Push “user is typing” or “new message” alerts without opening a full WebSocket channel.
- Progress monitoring: Show real‑time job progress for background tasks (e.g., Celery workers).
- IoT telemetry: Feed lightweight device updates to a monitoring console.
Testing and Debugging SSE
Browser DevTools
Open the Network tab, filter by “event‑source”, and you’ll see the continuous stream. Look for:
- Correct
Content-Type: text/event-stream. - Absence of
Transfer‑Encoding: chunkederrors (some proxies mishandle chunked streams). - Heartbeat comments keeping the connection alive.
Command‑Line Testing
Use curl to verify the raw stream:
curl -N http://localhost:5000/stream
The -N flag disables buffering, letting you see each event as it arrives.
Scaling SSE with Multiple Workers
Because each SSE connection holds a thread (Flask) or an async task (FastAPI), you’ll eventually need a process manager:
- Gunicorn with Gevent: Enables cooperative multitasking for Flask.
- Uvicorn workers: Use
uvicorn myapp:app --workers 4for FastAPI.
For truly massive scale, consider a message broker (Redis
Leave a Reply