{"id":439,"date":"2026-07-09T13:03:51","date_gmt":"2026-07-09T13:03:51","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/sse-vs-websockets-2026-the-real-time-channel-i-actually-ship\/"},"modified":"2026-07-09T13:03:51","modified_gmt":"2026-07-09T13:03:51","slug":"sse-vs-websockets-2026-the-real-time-channel-i-actually-ship","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/sse-vs-websockets-2026-the-real-time-channel-i-actually-ship\/","title":{"rendered":"SSE vs WebSockets in 2026: The Real-Time Channel I Actually Ship"},"content":{"rendered":"<p>I spent most of last year defaulting to WebSockets for anything that needed updates in real time. A new order came in, a price ticked, a job status changed. WebSocket. Two months ago I was building a small monitoring dashboard for a Laravel app. I wired up a WebSocket server, added a client library, added reconnection logic, added ping\/pong handling, added the whole thing, and then paused and asked what I was actually doing. The dashboard only needed the server to push updates. The client had nothing useful to say back.<\/p>\n<p>I ripped out the WebSocket setup, wrote about twelve lines of Server-Sent Events, and shipped it that afternoon. It&rsquo;s still running. Nothing broke. Nothing needed a reconnection library. I felt a little dumb, in a good way.<\/p>\n<p>So here&rsquo;s my honest take on when to pick which in 2026, based on shipping a bunch of these across Laravel, Node, and a Rust axum service.<\/p>\n<h2 id=\"the-one-question-that-actually-decides-it\">The one question that actually decides it<\/h2>\n<p>Does the client need to send anything meaningful back?<\/p>\n<p>If yes, WebSockets. Chat rooms, collaborative editors, multiplayer games, terminals. You need bidirectional traffic, so use the tool built for that.<\/p>\n<p>If no, SSE. Dashboards, feeds, LLM token streams, notifications, progress bars, log tails. The server pushes, the client listens.<\/p>\n<p>That&rsquo;s the whole decision. Everything else is optimization detail. I&rsquo;ve watched teams tie themselves in knots over &ldquo;what if we might need bidirectional later?&rdquo; You know what SSE has for that? A regular HTTP POST to a different endpoint. Same server, same auth, same middleware stack. Browsers turn out to be pretty good at making HTTP requests.<\/p>\n<h2 id=\"sse-in-about-twelve-lines-of-node\">SSE, in about twelve lines of Node<\/h2>\n<p>Here&rsquo;s the shape of it in Node with Express, which is where most people meet it first:<\/p>\n<pre><code class=\"language-js\">app.get(&quot;\/events&quot;, (req, res) =&gt; {\n  res.set({\n    &quot;Content-Type&quot;: &quot;text\/event-stream&quot;,\n    &quot;Cache-Control&quot;: &quot;no-cache&quot;,\n    &quot;Connection&quot;: &quot;keep-alive&quot;,\n    &quot;X-Accel-Buffering&quot;: &quot;no&quot;,\n  });\n  res.flushHeaders();\n\n  const send = (data) =&gt; res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n  const interval = setInterval(() =&gt; send({ ts: Date.now() }), 1000);\n\n  req.on(&quot;close&quot;, () =&gt; clearInterval(interval));\n});\n<\/code><\/pre>\n<p>And the client:<\/p>\n<pre><code class=\"language-js\">const es = new EventSource(&quot;\/events&quot;);\nes.onmessage = (e) =&gt; console.log(JSON.parse(e.data));\n<\/code><\/pre>\n<p>That&rsquo;s a functioning real-time channel. No handshake protocol, no ping frames, no library. The browser handles reconnection automatically, and if you set an <code>id:<\/code> on your events it will send <code>Last-Event-ID<\/code> back on reconnect so you can resume cleanly. It&rsquo;s closer to reading a slow file than to doing networking.<\/p>\n<p>For comparison, the equivalent WebSocket setup means picking a library (ws, socket.io, uWebSockets), configuring a server, handling upgrade requests, defining your message format, implementing ping\/pong, deciding on JSON or MessagePack, wiring auth through a different path than the rest of your HTTP stack. All of that is fine and worth it when you actually need it. It&rsquo;s not fine when you don&rsquo;t.<\/p>\n<h2 id=\"when-websockets-earn-their-keep\">When WebSockets earn their keep<\/h2>\n<p>I still reach for WebSockets when the client is producing events at unpredictable intervals, not just consuming them. Chat is the obvious example. Cursors in a shared doc, live audio metadata, IoT telemetry from a device. If the traffic pattern is bursty and I want a single long-lived connection rather than the client re-opening streams, that&rsquo;s a WebSocket job too. Games. Trading UIs.<\/p>\n<p>Binary transport is another clean split. SSE is text-only, per the spec. If you&rsquo;re streaming Protobuf frames or audio chunks, WebSockets. And if latency on the return path really matters, yes, a POST also works in theory, but at 60 messages per second in either direction you want the connection you already have.<\/p>\n<p>For the Laravel side of this, I wrote about swapping Pusher for <a href=\"https:\/\/abrarqasim.com\/blog\/laravel-reverb-in-production-what-i-pulled-out-of-pusher\/\" rel=\"noopener\">Laravel Reverb<\/a>, which is a legitimate WebSocket use case: broadcasts, presence, both directions.<\/p>\n<h2 id=\"the-scaling-story-most-tutorials-skip\">The scaling story most tutorials skip<\/h2>\n<p>The thing that trips up almost everyone with SSE is this: <strong>browsers cap HTTP\/1.1 connections to six per origin.<\/strong><\/p>\n<p>If your site opens an SSE stream on <code>\/events<\/code> and the user opens six tabs, tab seven can&rsquo;t open a new stream. It just hangs. This is not an SSE-specific problem, it&rsquo;s an HTTP\/1.1 problem, but SSE makes you notice it because you&rsquo;re holding the connection open indefinitely.<\/p>\n<p>Two fixes:<\/p>\n<ol>\n<li>Serve SSE over HTTP\/2 or HTTP\/3. The per-origin cap stops mattering because HTTP\/2 multiplexes many streams over one TCP connection. This is the default answer in 2026. If you&rsquo;re behind Nginx, Cloudflare, or Vercel, you probably already have HTTP\/2 in front of the browser.<\/li>\n<li>Put SSE on a subdomain like <code>events.yourapp.com<\/code> so it has its own connection budget. Useful when you can&rsquo;t control the edge proxy.<\/li>\n<\/ol>\n<p>The other thing that surprises people is proxy buffering. Nginx will happily hold your SSE payload in memory until it feels like flushing. Setting <code>X-Accel-Buffering: no<\/code> on the response tells it to stop. Most CDNs have a similar knob.<\/p>\n<p>WebSockets have a different pain: some corporate proxies still don&rsquo;t like the Upgrade dance and will kill the connection. SSE looks like a normal HTTP response with a <code>text\/event-stream<\/code> MIME type, so it slips through almost everywhere. That&rsquo;s a legitimate reason to prefer SSE inside enterprise networks.<\/p>\n<h2 id=\"llms-quietly-made-sse-the-default-for-ai-streaming\">LLMs quietly made SSE the default for AI streaming<\/h2>\n<p>Every LLM API I use ships tokens over SSE. OpenAI. Anthropic. The local vLLM endpoint I run on a workstation. Not WebSockets. That&rsquo;s not accidental. Token streaming is server-push. The client sends one request and reads. It&rsquo;s the natural fit. The <a href=\"https:\/\/html.spec.whatwg.org\/multipage\/server-sent-events.html\" rel=\"nofollow noopener\" target=\"_blank\">WHATWG SSE spec<\/a> shows how boringly small the wire format actually is: <code>data:<\/code> lines terminated by a blank line, optional <code>id:<\/code> and <code>event:<\/code> fields, done.<\/p>\n<p>When I&rsquo;m building the frontend for something that talks to an LLM, I use the browser&rsquo;s <code>EventSource<\/code> for anonymous streams or a fetch-based reader when I need auth headers. <code>EventSource<\/code> famously does not support custom headers, which is a real limitation to know before committing. If you&rsquo;re building AI features on top of a Node backend, most SDKs already return an async iterator over the SSE stream. The <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Server-sent_events\" rel=\"nofollow noopener\" target=\"_blank\">MDN Server-Sent Events guide<\/a> covers the client side well.<\/p>\n<p>For type-safe streaming APIs I&rsquo;ve been reaching for the same shape I described in my <a href=\"https:\/\/abrarqasim.com\/blog\/trpc-in-2026-the-type-safe-api-i-actually-reach-for\/\" rel=\"noopener\">tRPC post<\/a>: a subscription with a server iterator that maps neatly onto an SSE response.<\/p>\n<h2 id=\"the-rough-rules-i-keep-on-a-sticky-note\">The rough rules I keep on a sticky note<\/h2>\n<p>Bidirectional traffic from the client? WebSocket. Binary payloads? WebSocket. Enterprise proxies in the path? SSE. LLM tokens? SSE. Anything I could implement over polling but want smoother? SSE. Want to reuse existing HTTP auth, cookies, middleware, and rate limiters without rethinking any of it? SSE. Team already runs Redis pub\/sub or Kafka and needs a fanout channel to browsers? Either. That decision goes back to the front-half question about who&rsquo;s talking.<\/p>\n<p>The failure mode I want to warn against is reflexive-WebSocket. It happens because the WebSocket ecosystem is louder. There are more libraries, more conference talks, more &ldquo;real-time&rdquo; branding on the marketing pages. SSE has been sitting in the browser since 2011 and gets almost no attention. It&rsquo;s fine to feel like you&rsquo;re picking the boring option. You are. That&rsquo;s the point.<\/p>\n<p>If you want to see how these show up in real projects I&rsquo;ve built, most of the client work I do around real-time features lives in <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">my project work<\/a>.<\/p>\n<h2 id=\"try-this-next-week\">Try this next week<\/h2>\n<p>Pick one WebSocket endpoint you already have that only pushes to the client. Rewrite it as SSE. See if the code shrinks. See if anything actually breaks. If nothing does, that&rsquo;s the new default for that shape of feature.<\/p>\n<p>If you do need bidirectional (I&rsquo;m not trying to talk you out of it when you need it), start from <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc6455\" rel=\"nofollow noopener\" target=\"_blank\">RFC 6455<\/a> and pick a library from the same top three you already know. Nothing dramatic has changed in WebSocket-land in 2026. That&rsquo;s part of what makes SSE feel like the interesting choice again.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>When to pick Server-Sent Events over WebSockets in 2026: a shipping engineer&#8217;s take on real-time channels for dashboards, LLM streams, and more.<\/p>\n","protected":false},"author":2,"featured_media":438,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"When to pick Server-Sent Events over WebSockets in 2026: a shipping engineer's take on real-time channels for dashboards, LLM streams, and more.","rank_math_focus_keyword":"server sent events vs websockets","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[147,35],"tags":[49,484,321,483,482,481,233,235,172,231],"class_list":["post-439","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-backend","category-web-development","tag-backend","tag-eventsource","tag-http","tag-llm-streaming","tag-real-time-2","tag-server-sent-events-2","tag-sse","tag-streaming","tag-web-development-2","tag-websockets"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/439","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=439"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/439\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/438"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=439"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=439"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=439"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}