EventSource API — Server-Sent Events (Client Side)
debt(d7/e3/b3/t7)
Closest to 'only careful code review or runtime testing' (d7). No detection tools are specified in detection_hints. The common mistakes — missing double newline, forgetting es.close(), Nginx buffering, missing CORS headers — are all silent failures that manifest only at runtime (dropped connections, stalled streams, CORS errors in browser console). No standard linter rule catches these; they surface only under real server conditions or careful review.
Closest to 'simple parameterised fix' (e3). The quick_fix is a small, localised change: add a header on the server side, ensure double newlines, call es.close() on unmount. Each individual mistake is a targeted one-to-three line fix (e.g., add X-Accel-Buffering header, fix newline terminator, add withCredentials), but taken together across server config and client code they are small refactors within a single component rather than single-line patches.
Closest to 'localised tax' (b3). The applies_to scope is web only, and EventSource usage is typically confined to a specific streaming feature (e.g., one endpoint and one client component). It doesn't permeate the entire codebase — only the SSE endpoint and the consuming component carry the configuration burden (Nginx config, PHP time limits, CORS headers). The rest of the application is unaffected.
Closest to 'serious trap' (t7). The misconception field explicitly states that developers treat EventSource and WebSockets as interchangeable. This contradicts mental models carried from WebSocket experience — SSE is server-to-client only, uses plain HTTP, auto-reconnects, but cannot send client data. Choosing it when bidirectional communication is needed requires an architectural swap. The Nginx buffering issue and CORS credential default are additional non-obvious surprises that contradict reasonable assumptions.
Also Known As
TL;DR
Explanation
new EventSource(url) opens a persistent HTTP connection where the server pushes text/event-stream formatted messages. The browser automatically reconnects with exponential backoff if the connection drops, sending the Last-Event-ID header so the server can resume from where it left off. EventSource only supports GET requests and plain text — no binary, no custom headers without a polyfill. For bidirectional communication use WebSockets. For streaming responses from a PHP backend, the server writes 'data: <message>\n\n' and flushes — each double-newline is one event. Named events (event: type\n) allow different listeners. SSE works through proxies better than WebSockets and requires no upgrade handshake.
Common Misconception
Why It Matters
Common Mistakes
- Not disabling Nginx output buffering — Nginx buffers upstream responses by default; add 'X-Accel-Buffering: no' header or proxy_buffering off in nginx config.
- Forgetting the double newline to terminate events — each SSE message must end with \n\n; a single \n continues the current event's data.
- Not handling the CORS case — EventSource sends credentials by default only to same-origin; for cross-origin, pass { withCredentials: true } and set appropriate CORS headers.
- Long-lived PHP scripts without max_execution_time override — SSE streams run indefinitely; set set_time_limit(0) at the start of the streaming script.
- Not closing EventSource when navigating away — forgetting to call es.close() leaves connections open on the server; attach cleanup to beforeunload or component unmount.
Code Examples
// ❌ Polling — inefficient, delayed, hammers the server
setInterval(async () => {
const res = await fetch('/api/notifications');
const data = await res.json();
if (data.length) showNotifications(data);
}, 3000); // New HTTP request every 3 seconds even when nothing changed
// ✅ EventSource — persistent connection, server pushes when ready
const es = new EventSource('/api/notifications/stream');
es.onmessage = (event) => {
const notification = JSON.parse(event.data);
showNotification(notification);
};
// Named events — different handlers per event type
es.addEventListener('order-shipped', (event) => {
updateOrderStatus(JSON.parse(event.data));
});
es.onerror = (err) => {
// Browser auto-reconnects — this fires on reconnect attempts too
if (es.readyState === EventSource.CLOSED) {
console.error('SSE connection permanently closed');
}
};
// Close when done (e.g. user navigates away)
window.addEventListener('beforeunload', () => es.close());
// PHP side:
// header('Content-Type: text/event-stream');
// header('Cache-Control: no-cache');
// header('X-Accel-Buffering: no'); // Nginx: disable buffering
// while (true) {
// echo 'data: ' . json_encode(getLatestEvent()) . "\n\n";
// ob_flush(); flush();
// sleep(1);
// }
Tags
Edits history 1 edit
- common_mistakes PF Media Bot Claude Opus 4.5 · 2 May 2026