Server-Sent Events (SSE)¶
Server-Sent Events (SSE) is a W3C standard for one-way push from server to browser over a persistent HTTP connection — simpler than WebSockets when you only need server-to-client updates.
How SSE works¶
The client opens a regular GET request. The server responds with
Content-Type: text/event-stream and streams newline-delimited event payloads
indefinitely. The browser's EventSource API reconnects automatically on
disconnect.
Basic SSE endpoint¶
import asyncio
from FasterAPI import Faster, StreamingResponse
app = Faster()
async def event_generator():
count = 0
while True:
count += 1
yield f"data: count={count}\n\n".encode()
await asyncio.sleep(1)
@app.get("/events")
async def sse_endpoint():
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no", # disable Nginx buffering
},
)
Event format¶
Each event is a block of field: value lines followed by a blank line:
Fields:
| Field | Purpose |
|---|---|
data |
Event payload (required) |
event |
Custom event type (default: message) |
id |
Last-event-ID for reconnect |
retry |
Reconnect delay in ms |
Typed JSON events¶
import json
import msgspec
async def stock_events():
while True:
price = {"symbol": "FAST", "price": 123.45}
yield f"event: price\ndata: {json.dumps(price)}\n\n".encode()
await asyncio.sleep(0.5)
@app.get("/stocks")
async def stock_stream():
return StreamingResponse(stock_events(), media_type="text/event-stream")
Client-side usage¶
const source = new EventSource("/events");
source.onmessage = (e) => {
console.log("Message:", e.data);
};
source.addEventListener("price", (e) => {
const data = JSON.parse(e.data);
console.log("Price update:", data);
});
source.onerror = () => {
console.log("Connection lost, reconnecting...");
};
Disconnect detection¶
The connection closes when the client disconnects, but the generator keeps yielding
until the next await. Check request.is_disconnected() or wrap in a try/except:
from FasterAPI import Request
async def live_feed(request: Request):
while True:
if await request.is_disconnected():
break
yield b"data: tick\n\n"
await asyncio.sleep(1)
@app.get("/feed")
async def feed(request: Request):
return StreamingResponse(live_feed(request), media_type="text/event-stream")
Multiple clients with a shared queue¶
import asyncio
from collections import defaultdict
subscribers: dict[int, asyncio.Queue] = {}
_next_id = 0
def publish(message: str):
for q in subscribers.values():
q.put_nowait(message)
async def subscribe(request: Request):
global _next_id
_next_id += 1
sid = _next_id
subscribers[sid] = asyncio.Queue()
try:
while True:
if await request.is_disconnected():
break
try:
msg = await asyncio.wait_for(subscribers[sid].get(), timeout=15)
yield f"data: {msg}\n\n".encode()
except asyncio.TimeoutError:
yield b": keepalive\n\n" # comment to prevent proxy timeout
finally:
del subscribers[sid]
@app.get("/notifications")
async def notifications(request: Request):
return StreamingResponse(subscribe(request), media_type="text/event-stream")
SSE vs WebSockets¶
| SSE | WebSocket | |
|---|---|---|
| Direction | Server → Client | Bidirectional |
| Protocol | HTTP | ws:// / wss:// |
| Auto-reconnect | Yes (browser) | No (manual) |
| Proxy support | Universal | Variable |
| Use case | Live feeds, notifications | Chat, gaming, collaboration |
Next steps¶
- WebSockets — bidirectional real-time communication.
- Custom Response Classes — StreamingResponse details.