<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Build In Public]]></title><description><![CDATA[Build In Public]]></description><link>https://wavellen.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6a073ae973afc887577f93a4/0970ac73-ddf1-4ea6-8b35-b19c9edb70b5.jpg</url><title>Build In Public</title><link>https://wavellen.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 30 Oct 2026 18:09:15 GMT</lastBuildDate><atom:link href="https://wavellen.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Why I chose SSE over WebSockets and the problem nobody tells you about EventSource]]></title><description><![CDATA[When I built the realtime dashboard, I had to choose b/w WebSockets & Server Sent Events. Most tutorials default to WebSockets. Here's why I didn't.]]></description><link>https://wavellen.hashnode.dev/why-i-chose-sse-over-websockets-and-the-problem-nobody-tells-you-about-eventsource</link><guid isPermaLink="true">https://wavellen.hashnode.dev/why-i-chose-sse-over-websockets-and-the-problem-nobody-tells-you-about-eventsource</guid><dc:creator><![CDATA[Wavellen]]></dc:creator><pubDate>Wed, 03 Jun 2026 17:54:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a073ae973afc887577f93a4/521985c2-da9c-48ef-928b-5d7c1f1555cc.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I built the real time dashboard for Uptime Pulse, I had to choose between WebSockets and Server Sent Events. Most tutorials default to WebSockets. Here is why I did not and the specific problem I ran into.</p>
<p>WebSockets are full duplex. Both server and client can send messages at any time. Perfect for chat or multiplayer games.</p>
<p>SSE is unidirectional which means that server pushes, client listens. That is it.</p>
<p>For a monitoring dashboard, the client never needs to push data through the real time channel. The server detects a status change and broadcasts it. SSE is the correct tool.</p>
<p>IF your client only needs to receive updates use SSE&gt; If it needs to send and receive use WebSockets.</p>
<p><strong>The problem nobody mentions which is EventSource can't send headers.</strong></p>
<p>The browser's native EventSource API is clean and handles auto re-connection natively . One problem, it does not support custom headers.</p>
<p>You cannot send Authorization: Bearer with EventSource. For any protected endpoint., that is a deal breaker.</p>
<pre><code class="language-javascript">// This does not work
const es = new EventSource('/stream', {
    headers: {Autorization: `Bearer ${token}`}
});
</code></pre>
<p><strong>The fix is fetch based SSE</strong></p>
<p>use fetch() with Authorization header and read the response body as a stream.</p>
<pre><code class="language-javascript">const response = await fetch(`monitors/${id}/stream`, {
    headers: { Authorization: `Bearer ${token}` }
});

const reader = response.body.getReader();
const decoder = new TextDecoder();

while(true) {
    const {done, value} = await reader.read();
    if(done) break;
    const text = decoder.decode(value);
    for (const line of text.split('\n')) {
        if(line.startsWith('data: ')) {
            const data = JSON.parse(line.slice(6));
            // handle the event
        }
    }
}

// cleanup on unmount
reader.cancel();
</code></pre>
<p>response.body.getReader() gives you a ReadableStreamDefaultReader. Unlike a normal fetch, it returns chunks as they arrive. Exactly how SSE works.</p>
<p><strong>The server side (Fastify)</strong></p>
<pre><code class="language-javascript">reply.raw.setHeader('Content-Type', 'text/event-stream');
reply.raw.setHeader('Cache-Control', 'no-cache');
reply.raw.setHeader('Connection', 'keep-alive');
reply.raw.flushHeaders();
reply.raw.write('data: {"type":"connected"}\n\n');
addClient(monitorId, reply);
request.raw.on('close', () =&gt; removeClient(monitorId, reply));
// do NOT call reply.send() — that closes the connection
</code></pre>
<p><strong>Write order matters:</strong></p>
<p>The worker always writes check to DB -&gt; Creates/resolves alert -&gt; Pushes SSE. SSE is always last. The dashboard reflects already persisted state, never in-memory state.</p>
<p>Full Project: <a href="https://github.com/wavellen/uptime-pulse">github.com/wavellen/uptime-pulse</a></p>
]]></content:encoded></item></channel></rss>