Server-Sent Events (SSE)
For one-way updates (notifications, progress bars, live dashboards) SSE is simpler than WebSockets: plain HTTP, automatic reconnection in the browser and it passes through Nginx and HTTP/2. Use WebSockets when the browser also needs to send messages.
1. A stream from a generator
Python
import asyncio
from lila.core.sse import EventStream
@router.get("/import/progress")
async def import_progress(request: Request):
async def steps():
for pct in range(0, 101, 10):
yield {"event": "progress", "data": {"pct": pct}}
await asyncio.sleep(0.5)
yield {"event": "done", "data": {"message": "Import finished"}}
return EventStream(request, steps())
2. Channels: publish from anywhere
Python
from lila.core.sse import Channel
from lila.core.session import Session
notifications = Channel("notifications")
@router.get("/events", auth=True)
async def events(request: Request):
user = request.state.user
return notifications.stream(request, key=f"user:{user['id']}") # broadcasts + this user's messages
# in a route, a task or a scheduled job:
await notifications.publish({"message": "New comment"}, event="notice") # everyone
await notifications.publish({"message": "Invoice paid"}, event="notice", to="user:7") # one user
- Heartbeats every 15 s keep proxies from closing idle streams; disconnected clients are detected and cleaned up.
- Slow clients never block others: each subscriber has a bounded queue (oldest messages dropped).
- Responses are never gzipped/buffered and carry
X-Accel-Buffering: nofor Nginx. - With
CACHE_DRIVER=redismessages reach subscribers in every worker (Redis pub/sub); otherwise they stay in the current process. - Authenticate the stream like any route (
auth=True,roles=[...]).
3. Browser
HTML
<!-- No JavaScript: append messages, show toasts -->
<ul data-sse="/events" data-sse-event="notice" data-sse-swap="append" data-sse-toast="News"></ul>
<!-- Progress bar -->
<div id="bar" class="h-2 bg-blue-600" style="width:0"></div>
<script>
var progress = lilaSSE("/import/progress");
progress.on("progress", (d) => { document.getElementById("bar").style.width = d.pct + "%"; });
progress.on("done", (d) => { lila("toast", { title: "OK", description: d.message, type: "success" }); progress.close(); });
</script>
data-sse-swap:append,prepend,innerortext.{"html": "..."}payloads are inserted as HTML, otherwisemessageis used as text.data-sse-callback="fn"callswindow.fn(data, element).- Streams opened by a page close when the SPA navigates away; add
data-sse-persist/{ persist: true }for an app-wide stream (e.g. in your layout).