HTML SSE
html sse server-sent events power one-way live updates in production dashboards and log t server-sent events (sse) — new eventsource(url)
Introduction
Server-Sent Events (SSE) — new EventSource(url) — opens a long-lived HTTP connection for server→client text streams. Staff engineers choose SSE over WebSockets when traffic is one-way (stock tickers, Netflix live status, sports scores, CI build logs) — simpler protocol, auto-reconnect, HTTP/2 multiplex friendly. Know reverse-proxy buffering (nginx), auth via cookies, and WCAG pause controls for live regions.
Business problem
Business pressure: Users expect live dashboards — Uber ETA updates, Robinhood price flashes, GitHub Actions log tail. Polling every 2s wastes server and battery; WebSockets add infra complexity for read-only feeds. SSE fits HTTP caching layers and load balancers better than WS — until bidirectional needed.
- Conversion: Live inventory counts ("3 left") drive urgency — SSE pushes without full page refresh.
- Compliance: Financial tick data may need audit log of stream auth — cookie session on same-origin SSE.
- SEO: Live content not indexed in real-time — SSR static snapshot; SSE enhances logged-in dashboards only.
Why this feature exists
Platform motivation: WHATWG EventSource (2006–2011) standardized comet/long-poll replacement with built-in Last-Event-ID reconnection. Simpler than WebSocket for push notifications, news tickers, and ChatGPT-style streaming text (over fetch streams now too).
- History: Predates fetch ReadableStream streaming; still widely deployed for log tail and metrics.
- Alternative rejected: Short polling — 10x request overhead on Hacker News scale live threads.
- Modern role: SSE for one-way; WebSocket for chat/games; fetch stream for LLM tokens — choose by direction and infra.
Browser internals
Inside the engine: EventSource creates persistent GET with Accept: text/event-stream. Parser reads SSE format: data: lines, optional event: type, id: for replay, double newline dispatches MessageEvent. On disconnect, browser waits retry ms from server or default and reconnects sending Last-Event-ID header.
- Parser: UTF-8 text only — binary needs WebSocket or fetch stream.
- CORS: Cross-origin SSE needs ACAO; cookies withCredentials for auth streams.
- Limits: Browser max 6 connections per domain HTTP/1.1 — HTTP/2 multiplex helps; still cap EventSource count.
// Server format// data: {"price": 142.50}// data: partial line// data: continuedconst es = new EventSource('/api/ticker');es.onmessage = e => updatePrice(JSON.parse(e.data));es.addEventListener('halt', () => es.close());
Rendering workflow
Rendering path: Each message handler updates DOM — unbatched updates on high-frequency tickers cause layout thrash. Batch with rAF or max 4 updates/sec display — Bloomberg terminal pattern throttles flash. aria-live="polite" on price region — not assertive on every cent change.
- Critical path: SSE connects after LCP — don't block render on EventSource open.
- Layout: Fixed-width tabular nums on live prices — prevents CLS from digit width change.
- Paint: Highlight flash on change — CSS animation 300ms, not permanent background toggle every tick.
Feature deep dive
SSE vs WebSocket vs polling: SSE: server→client, text, auto-reconnect, HTTP. WS: bidirectional, binary, manual reconnect. Polling: dumb but firewall-friendly. SSE auth: HttpOnly cookie on same-origin; token in query fragile (logs). Close EventSource on page hide to save battery — Page Visibility API.
- Proxy: nginx proxy_buffering off for SSE location — or events batch delayed 30s.
- Heartbeat: Comment lines
: pingkeep connection alive through proxies. - Named events: event: stockUpdate — addEventListener vs onmessage.
<div id="price" aria-live="polite" aria-atomic="true">—</div><div id="conn" role="status">Connecting…</div><script type="module">const es = new EventSource('/api/stream/prices');es.onopen = () => document.getElementById('conn').textContent = 'Live';es.onerror = () => document.getElementById('conn').textContent = 'Reconnecting…';document.addEventListener('visibilitychange', () => {if (document.hidden) es.close();else location.reload(); // or reopen EventSource});</script>
Accessibility analysis
A11y architecture: WCAG 2.2.2 Pause, Stop, Hide — auto-updating stock ticker needs pause button. aria-live polite batches announcements — NVDA floods if assertive on every SSE message. Provide static snapshot table as alternative — don't rely on live stream alone for critical info.
- Screen readers: Throttle live region updates to meaningful changes (>1% price move).
- Keyboard: Pause stream button first in toolbar — ESPN live score pattern.
- WCAG: 2.3.1 Three Flashes — rapid SSE-driven color flash limits.
SEO impact
SEO architecture: Live SSE content invisible to crawlers — index static HTML baseline. News sites SSR headline; SSE updates breaking tag client-side — Google sees SSR version. Don't gate indexable article body behind SSE-only load.
- Crawl: Canonical content in HTML source; SSE is enhancement.
- Rich results: LiveBlogPosting schema updated server-side for AMP live blogs — not client SSE alone.
- CWV: Long connection doesn't hurt LCP if deferred post-load.
Security considerations
Security boundary: SSE over GET — auth token in URL appears in logs and Referer — use cookies. XSS can open EventSource to attacker's stream — exfiltrate session if misconfigured CORS. Rate-limit connections per user server-side — SSE exhaustion DoS.
- CSP: connect-src must allow stream endpoint.
- CSRF: Cookie-auth SSE same-site — SameSite=Lax usually sufficient for GET stream.
- Injection: If server echoes user content in data: field without encoding — client JSON.parse safe but HTML injection if carelessly rendered.
Performance impact
Performance: One EventSource per feed — multiplex events in one stream vs 20 connections. Mobile: close on background tab — iOS kills idle connections anyway. Server: hold thousands of open SSE connections — async Node/Go servers; thread-per-connection Java fails.
- INP: Handler parses JSON + DOM update 60/sec — throttle to 4Hz display.
- Memory: Append-only log UI — virtualize list, trim old events.
- Network: HTTP/2 single connection multiplexes SSE + assets — prefer HTTP/2 origin.
Real production example
GitHub Actions log tail / Netflix playback status: SSE or chunked HTTP stream delivers lines; UI appends to virtualized terminal; reconnect on drop with Last-Event-ID resumes from line 4000. nginx location with proxy_buffering off and 86400s read timeout.
- infra: ALB idle timeout > heartbeat interval — AWS 4000s default kills silent SSE.
- Fallback: Poll every 5s if EventSource undefined — corporate proxy blocks streaming.
- Monitoring: Client reconnect count metric — spikes indicate proxy misconfig.
# nginx SSE locationlocation /api/stream/ {proxy_pass http://backend;proxy_buffering off;proxy_cache off;proxy_read_timeout 86400s;chunked_transfer_encoding off;}
Enterprise usage
Enterprise: Corporate proxies block long-lived connections — detect onerror loop, degrade to poll. SSO session expiry mid-stream — server sends event: logout; client closes ES and redirects. Financial feeds audit Last-Event-ID replay for compliance.
- Design system: LiveRegion component with pause, throttle, connection status badge.
- Ops: Runbook for nginx buffering causing "stale dashboard" tickets.
- CI: Integration test opens EventSource against test server — assert event sequence.
Common production failures
What breaks in prod: nginx proxy_buffering batches 30s of "live" data; ALB idle timeout drops silent streams; 6 SSE connections exhaust HTTP/1.1 limit; aria-live assertive spam; token in query string in CDN logs.
- Incident: Trading dashboard showed prices 45s delayed — nginx buffering default on.
- Mobile: Background tab SSE killed — users saw stale data until manual refresh — fixed with visibility reconnect.
- a11y: Screen reader users couldn't use live scoreboard — no pause, filed complaint.
Architecture review questions
- Is nginx/proxy buffering disabled for SSE route?
- Is there pause control for auto-updating content?
- Are DOM updates throttled to prevent INP/layout thrash?
- Is auth via cookie not URL token?
- Is EventSource closed on page hide / unmount?
- Is there polling fallback for blocked streams?
Hands-on project
Project: Live metric dashboard: EventSource consumer, pause button, rAF-throttled DOM, connection status, nginx config doc, poll fallback.
- Deliverable: aria-live polite; close on hidden tab; Last-Event-ID resume demo.
- Verify: Simulate proxy buffering off; axe pause control present.
- Stretch: Compare fetch ReadableStream vs EventSource for same feed.
Interview questions
SSE vs WebSocket — decision criteria?(Advanced)
SSE: one-way server push, text, auto-reconnect, standard HTTP, simpler LB. WebSocket: bidirectional, binary, gaming/chat/collab. SSE loses if client must send frequent messages over same channel.
Follow-up: SSE over HTTP/2 benefits?
Why do SSE streams stall behind nginx?(Advanced)
proxy_buffering accumulates response until buffer full — disable for text/event-stream location. Also check read timeouts and ALB idle timeout vs heartbeat comment lines.
Follow-up: Heartbeat implementation?
Make SSE accessible for a live ticker.(Advanced)
Pause button; aria-live polite throttled; aria-atomic on summary region; static table fallback; avoid flash; keyboard reachable pause first.
Follow-up: WCAG 2.2.2 requirements?
Try it yourself
Edit the HTML, CSS, or JS panels — the preview updates as you type.
Try it yourself
Summary
Server-Sent Events power one-way live updates in production dashboards and log tails — with nginx buffering off, connection lifecycle management, throttled aria-live regions, pause controls, and WebSocket reserved for bidirectional workloads.