Server-Sent Events

One-Way Streaming Over Plain HTTP

Server-Sent Events

Server-Sent Events are a long-lived HTTP response with content type text/event-stream. The browser's EventSource API auto-reconnects on disconnect.

4 min read Level 3/5 #fastify#sse#realtime
What you'll learn
  • Set Content-Type to text/event-stream
  • Write data lines followed by two newlines
  • Clean up on client disconnect

SSE is the simplest realtime channel: a single long-lived HTTP response that the server keeps writing into. The browser’s EventSource parses the stream and dispatches events.

A Minimal SSE Endpoint

app.get('/events', (req, reply) => {
  reply.raw.setHeader('Content-Type', 'text/event-stream')
  reply.raw.setHeader('Cache-Control', 'no-cache, no-transform')
  reply.raw.setHeader('Connection', 'keep-alive')
  reply.raw.flushHeaders()

  const interval = setInterval(() => {
    reply.raw.write(`data: ${JSON.stringify({ now: Date.now() })}\n\n`)
  }, 1000)

  req.raw.on('close', () => clearInterval(interval))
})

Two newlines mark the end of an event. A single line beginning with data: is the payload.

Named Events & IDs

function send(reply: FastifyReply, event: string, data: unknown, id?: string) {
  if (id) reply.raw.write(`id: ${id}\n`)
  reply.raw.write(`event: ${event}\n`)
  reply.raw.write(`data: ${JSON.stringify(data)}\n\n`)
}

app.get('/notifications', (req, reply) => {
  reply.raw.setHeader('Content-Type', 'text/event-stream')
  reply.raw.flushHeaders()
  send(reply, 'welcome', { user: 'alice' }, '1')
})

Clients listen with source.addEventListener('welcome', handler) and ignore other event types.

Why SSE Over WebSockets?

When you only need server-to-client messages, SSE is simpler: it works over plain HTTP, traverses proxies, and reconnects automatically using the last id. For bidirectional needs, choose WebSockets instead.

@fastify/websocket →