Build an SSE stream

Frame an event stream

Return the correct content type and encode events as lines separated by a blank line.

An SSE response is plain HTTP that never ends. The server sets Content-Type: text/event-stream and writes events. Each event is line based text, and a blank line tells the browser “dispatch this one now.”

Writing raw JSON without framing looks live in one tab and breaks everywhere else.

Headers and one event

const http = require('http')

const server = http.createServer((req, res) => {
  if (req.url !== '/events') {
    res.writeHead(404)
    res.end()
    return
  }

  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    Connection: 'keep-alive',
  })

  res.write('event: incident.created\n')
  res.write('id: evt_85\n')
  res.write('data: {"id":42,"title":"Database failover"}\n\n')

  req.on('close', () => res.end())
})

server.listen(3000)

Start the server, then inspect the raw bytes:

curl -N http://localhost:3000/events

You should see the three field lines, then a blank line, then the connection stays open. Two events means two blank-line-terminated blocks, not one concatenated JSON blob.

Client side

The browser API is EventSource:

const source = new EventSource('/events')

source.addEventListener('incident.created', (event) => {
  console.log(event.lastEventId, JSON.parse(event.data))
})

When the second event arrives, the console prints evt_85 and the parsed object. That is the proof framing worked.

Common mistake

Dumping {"id":42}{"id":43} into the body without data: lines and \n\n separators will not produce two browser events. The parser needs the field lines.

Keep the first version tiny: one route, two hard-coded events, curl plus one browser tab. Add auth and replay after the framing is boring.

Wrong framing looks like this

If you res.write(JSON.stringify({ id: 42 })) without data: lines, curl shows one blob and the browser never fires incident.created. Fixing headers alone will not help until the body uses field lines plus \n\n.

Multi-line JSON belongs in one data: line or several data: lines the spec joins together. Do not pretty-print raw JSON without the prefix.

Try this on your own project: emit two named events from one endpoint, curl the stream, and confirm the browser fires two separate listener calls.

Lesson completed