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.
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.