DEV Community

Ujjawal Pandey
Ujjawal Pandey

Posted on

Taking a Deep Dive on Server-Sent Events (SSE)

Outline

Here's the menu for today. Grab some coffee, this one's worth it:

  1. The "wait, how does that work?" moment
  2. Origin story (spoiler: it involves pizza)
  3. The core of SSE, text/event-stream and EventSource
  4. A basic example (client + server)
  5. SSE vs Webhooks
  6. SSE vs WebSockets
  7. Who's using it, and when YOU should use it

Context

You're sitting there, you've typed out a prompt on Claude. Something deep. Something meaningful. Something like "write me a haiku about my backend crashing on a Friday evening."

You hit Enter.

Jim Carrey typing furiously gif

And then, the words don't just appear. They start flowing in. One after the other.

Ever wondered how that happens? Why isn't the entire paragraph just dumped on your screen the second the model is done cooking?

That, my friend, is Server-Sent Events doing its quiet, underrated magic. And once you understand it, you'll start seeing it everywhere, stock tickers, live sports scores, notifications, AI chatbots. It's been hiding in plain sight.

Let's deep dive.


Origin Story: The Pizza Problem

Picture this. It's the year 2000. You're in Italy. You've just ordered a pizza over a landline call (yes, dial tones and all). Now you're hungry, anxious, and wondering, where the hell is my pizza?

Option 1, The Polling Way:
Every 10 minutes, you pick up the phone, dial the restaurant, and ask for the updates on pizza delivery. The restaurant picks up, checks with the delivery guy, tells you "5 more minutes," and hangs up. You repeat this. Again. And again.

Cost? Every call burns your phone credit. Also, 80% of those calls are wasted because nothing changed since the last one.

Delivery guy stunting

Option 2, The SSE Way:
You're a loyal customer. The pizzeria knows you. So they tell the delivery guy: "Hey, every bus stop you pass, give him a call. Just a quick one, 'crossed Piazza Navona,' 'near the fountain,' 'two streets away.' He doesn't need to say anything back. Just listen."

Now look at this from the cost side:

  • You don't spend a rupee. You're just receiving updates.
  • The restaurant is rich (they sell pizza in Rome, come on), so the calls cost them nothing meaningful.
  • Both sides save time. No wasted "is it there yet?" calls. The connection stays open. The information flows one way, from the restaurant to you. You just listen.

That's Server-Sent Events. The server keeps a connection open with your browser, and whenever it has something new to say, it pushes it down. You don't ask. You just receive.


What's Happening in the Console

Open DevTools, hit the Network tab, send a prompt to Claude, and look at the request. You'll see something like this:

Request URL: https://claude.ai/api/.../completion
Content-Type: text/event-stream
Enter fullscreen mode Exit fullscreen mode

Content-Type in Console

And if you peek at the response stream, it looks like this:

event: message_start
data: {"type":"message_start","message":{...}}

event: content_block_delta
data: {"type":"content_block_delta","delta":{"text":"Hello"}}

event: content_block_delta
data: {"type":"content_block_delta","delta":{"text":" there"}}

event: content_block_delta
data: {"type":"content_block_delta","delta":{"text":"!"}}

event: message_stop
data: {"type":"message_stop"}
Enter fullscreen mode Exit fullscreen mode

Chunks coming from Claude

Each chunk is an event. Each event has data. The connection stays open, and the server keeps firing these small packets as the LLM generates tokens. Your browser picks them up and paints them on screen, that's why you see the typewriter effect.

No magic. Just a long-lived HTTP connection with a specific content type.


The Core Components

SSE is built on two dead-simple things:

1. Content-Type: text/event-stream

This is the secret handshake. When the server sends back a response with this header, the browser goes, "Oh okay, this isn't a normal response. Don't close the connection. Keep reading."

The response body follows a specific format:

event: <event-name>
data: <your payload>
id: <optional event id>
retry: <optional reconnect time in ms>

Enter fullscreen mode Exit fullscreen mode

Each event ends with a blank line. That's the delimiter. Miss the blank line and the browser just sits there confused like me in a linear algebra class.

2. EventSource (the client side)

The browser has a built-in API called EventSource. You don't need fetch, you don't need WebSockets, you don't need a third-party library. It's built in. It even handles automatic reconnection if the connection drops.

const source = new EventSource('/stream');

source.onmessage = (event) => {
  console.log('Got:', event.data);
};
Enter fullscreen mode Exit fullscreen mode

That's it. That's the whole client.


Bare-Bones Example

Let's build the smallest possible SSE thing. Server in Node/Express, client in plain HTML.

Server (Node + Express)

const express = require('express');
const app = express();

app.get('/stream', (req, res) => {
  // The magic headers
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  let count = 0;

  const interval = setInterval(() => {
    count++;
    // Note the format: "data: <stuff>\n\n"
    res.write(`data: Message number ${count}\n\n`);

    if (count >= 10) {
      clearInterval(interval);
      res.end();
    }
  }, 1000);

  // Cleanup if client disconnects
  req.on('close', () => {
    clearInterval(interval);
  });
});

app.listen(3000, () => console.log('SSE server running on 3000'));
Enter fullscreen mode Exit fullscreen mode

Client (plain HTML + JS)

<!DOCTYPE html>
<html>
<body>
  <div id="output"></div>
  <script>
    const source = new EventSource('http://localhost:3000/stream');
    const output = document.getElementById('output');

    source.onmessage = (e) => {
      output.innerHTML += `<p>${e.data}</p>`;
    };

    source.onerror = (e) => {
      console.log('Connection closed or errored');
      source.close();
    };
  </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Boom. That's a working SSE setup in ~30 lines. No libraries, no WebSocket drama, no polling hell.


SSE vs Webhooks

People mix these up all the time. They're both "push" mechanisms but they live in completely different universes.

sse vs webhook

SSE Webhooks
Who's listening? A user's browser (client-side) Another server (server-to-server)
Connection One long-lived HTTP connection, server pushes as things happen Server calls a URL you registered, one request per event
Use case Live UI updates (chat streams, notifications, dashboards) Async backend events (Stripe payment success, GitHub push, etc.)

TL;DR, SSE is for your users' screens. Webhooks are for your servers talking to each other.


SSE vs WebSockets

This is the one that trips people up the most. Both keep connections open, both push data. So why pick one over the other?

SSE WebSockets
Direction One-way (server → client) Two-way (both sides can talk anytime)
Protocol Plain HTTP, works with proxies, load balancers, CDNs out of the box Upgraded TCP connection (ws:// / wss://), sometimes a pain with infra
Complexity Native browser API, auto-reconnect built in You manage reconnection, heartbeats, framing yourself (or use a lib)

Rule of thumb: If data only needs to flow down to the user, SSE is simpler, cheaper, and good enough. If you need real back-and-forth (like a multiplayer game or a collab editor), go WebSockets.

Using WebSockets for a stock ticker is like using a Ferrari to go buy milk from the kirana store. It works, but why are you like this.


Where SSE Actually Shines

Who's using it in the wild?

  • GitHub, live updates on workflow runs and notifications in some parts of the UI.
  • Mapbox, various trading platforms, for pushing live price ticks and location updates.
  • Vercel, Netlify dashboards, streaming build logs straight to your browser as they happen.

First-principles: when should you reach for SSE?

Ask yourself three questions:

  1. Does data only need to flow one way, from server to client? If yes, you don't need the overhead of WebSockets.
  2. Is the data event-driven and unpredictable in timing? If yes, polling is wasteful. SSE is perfect.
  3. Am I okay with HTTP's existing infrastructure? If yes (and you probably are -- HTTPS, CDNs, auth headers all just work), SSE gives you all that for free. If you ticked all three, SSE is your guy.

If you found this useful, drop a ❤️ and share it with someone who still thinks setInterval + fetch is a reasonable way to build a live feed. We can do better.

Top comments (0)