Skip to content

SSE with Node.js HTTP — Complete Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about SSE with Node.js HTTP. We cover key concepts, practical examples, and best practices to help you master this topic.

Implement Server-Sent Events with raw Node.js HTTP: create SSE servers, set headers, stream events, handle client disconnection, manage multiple connections, and broadcast events.

What You Learn

You will learn how to implement SSE endpoints using Node.js built-in http module, manage connected clients, handle disconnection, broadcast events, and build a reusable SSE server class.

Why It Matters

Understanding SSE at the raw HTTP level reveals how it works under the hood. The http module approach works without frameworks, making it portable across any Node.js application and useful for understanding Express SSE internals.

Real-World Use

DodaTech's lightweight status server uses raw Node.js HTTP for SSE. The server pushes health check results to monitoring tools. No frameworks needed, just 50 lines of code for a production SSE endpoint.

Basic SSE Server

const http = require('http');

const server = http.createServer((req, res) => {
    if (req.url === '/events') {
        res.writeHead(200, {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            'Connection': 'keep-alive',
        });

        let counter = 0;
        const interval = setInterval(() => {
            counter++;
            const data = JSON.stringify({
                count: counter,
                timestamp: Date.now(),
            });
            res.write(`id: ${counter}\ndata: ${data}\n\n`);

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

        req.on('close', () => {
            clearInterval(interval);
            console.log('Client disconnected');
        });
    } else {
        res.writeHead(404);
        res.end();
    }
});

server.listen(3000, () => {
    console.log('SSE server on http://localhost:3000/events');
});

Client Manager Class

const http = require('http');

class SSEServer {
    constructor() {
        this.clients = new Set();
        this.clientId = 0;
    }

    handleConnection(req, res) {
        if (req.url !== '/events') {
            res.writeHead(404);
            res.end();
            return;
        }

        this.clientId++;
        const clientInfo = {
            id: this.clientId,
            res,
            connectedAt: Date.now(),
            userAgent: req.headers['user-agent'],
        };

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

        res.write(`event: connected\ndata: ${JSON.stringify({
            clientId: clientInfo.id,
            message: 'SSE connection established'
        })}\n\n`);

        this.clients.add(clientInfo);
        console.log(`Client ${clientInfo.id} connected (${this.clients.size} total)`);

        req.on('close', () => {
            this.clients.delete(clientInfo);
            console.log(`Client ${clientInfo.id} disconnected (${this.clients.size} remaining)`);
        });
    }

    broadcast(event, data) {
        const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
        for (const client of this.clients) {
            client.res.write(message);
        }
    }

    sendTo(clientId, event, data) {
        for (const client of this.clients) {
            if (client.id === clientId) {
                client.res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
                return true;
            }
        }
        return false;
    }

    getClientCount() {
        return this.clients.size;
    }

    getClients() {
        return Array.from(this.clients).map(c => ({
            id: c.id,
            connectedAt: c.connectedAt,
            userAgent: c.userAgent,
        }));
    }
}

const sseServer = new SSEServer();

const server = http.createServer((req, res) => {
    if (req.url === '/events') {
        sseServer.handleConnection(req, res);
    } else if (req.url === '/status' && req.method === 'GET') {
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({
            clients: sseServer.getClientCount(),
            clientList: sseServer.getClients(),
        }));
    } else {
        res.writeHead(404);
        res.end();
    }
});

setInterval(() => {
    sseServer.broadcast('heartbeat', {
        time: Date.now(),
        clients: sseServer.getClientCount(),
    });
}, 5000);

server.listen(3000);

Keep-Alive and Reconnection

const http = require('http');

const clients = new Set();

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

    const lastEventId = req.headers['last-event-id'];
    console.log(`Client connecting, last event: ${lastEventId || 'none'}`);

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

    // Send retry interval (5 seconds)
    res.write('retry: 5000\n\n');

    if (lastEventId) {
        res.write(`event: resuming\ndata: ${JSON.stringify({ fromId: lastEventId })}\n\n`);
    }

    clients.add(res);

    // Heartbeat every 15 seconds
    const heartbeat = setInterval(() => {
        res.write(': heartbeat\n\n');
    }, 15000);

    req.on('close', () => {
        clients.delete(res);
        clearInterval(heartbeat);
    });

    // Send events
    let eventId = parseInt(lastEventId) || 0;
    const stream = setInterval(() => {
        eventId++;
        const data = JSON.stringify({
            id: eventId,
            time: Date.now(),
        });
        res.write(`id: ${eventId}\ndata: ${data}\n\n`);
    }, 2000);

    req.on('close', () => {
        clearInterval(stream);
    });
});

server.listen(3000);

Event History and Replay

const http = require('http');

class SSEEventStore {
    constructor(maxEvents = 100) {
        this.events = [];
        this.maxEvents = maxEvents;
        this.counter = 0;
    }

    addEvent(event, data) {
        this.counter++;
        const entry = {
            id: this.counter,
            event,
            data,
            timestamp: Date.now(),
        };
        this.events.push(entry);

        if (this.events.length > this.maxEvents) {
            this.events.shift();
        }

        return entry;
    }

    getEventsFrom(lastId) {
        const startId = parseInt(lastId) || 0;
        return this.events.filter(e => e.id > startId);
    }
}

const eventStore = new SSEEventStore();
const clients = new Set();

const server = http.createServer((req, res) => {
    if (req.url === '/stream') {
        const lastEventId = req.headers['last-event-id'];

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

        // Replay missed events
        if (lastEventId) {
            const missedEvents = eventStore.getEventsFrom(lastEventId);
            for (const event of missedEvents) {
                res.write(`id: ${event.id}\nevent: ${event.event}\ndata: ${JSON.stringify(event.data)}\n\n`);
            }
        }

        clients.add(res);

        req.on('close', () => {
            clients.delete(res);
        });
    } else if (req.url === '/publish' && req.method === 'POST') {
        let body = '';
        req.on('data', chunk => body += chunk);
        req.on('end', () => {
            const { event, data } = JSON.parse(body);
            const entry = eventStore.addEvent(event, data);

            const message = `id: ${entry.id}\nevent: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
            for (const client of clients) {
                client.write(message);
            }

            res.writeHead(200);
            res.end(JSON.stringify({ id: entry.id, sent: clients.size }));
        });
    } else {
        res.writeHead(404);
        res.end();
    }
});

server.listen(3000);

Common Mistakes

1. Not Calling res.end()

SSE connections should stay open. Only call res.end() when intentionally closing the stream. For long-lived streams, never call res.end().

2. Missing Keep-Alive

Without periodic data or comments, proxies time out idle connections. Send a comment line (: ...) every 15-30 seconds.

3. Not Handling Backpressure

If the client is slow, res.write() buffers internally. For high-throughput streams, monitor res.destroyed and res.writableEnded.

4. No Error Handling on res.write()

If the client disconnects, res.write() throws an error. Wrap in try/catch or check res.destroyed before writing.

5. Memory Leaks from Abandoned Clients

Clients that disconnect without close event leave stale references. Always clean up in req.on('close').

Practice Questions

1. How does raw Node.js SSE detect client disconnection?

Listen for the req.on('close') event. This fires when the client disconnects or the socket times out.

2. What is the purpose of the 'retry' field in SSE?

It tells the browser how many milliseconds to wait before reconnecting after disconnection. Set it early in the stream.

3. How do you broadcast events to all connected clients?

Maintain a Set of response objects. Iterate over the set and call res.write() on each. Remove clients on close.

4. How do you replay missed events on reconnection?

Track the Last-Event-ID from the request headers. Query an event store for events with IDs greater than that value and send them.

Challenge

Build a raw Node.js SSE server with: event history (last 50 events stored in memory), replay on reconnection using Last-Event-ID, broadcast via POST /publish endpoint, client listing at GET /clients, heartbeat every 10 seconds, and a maximum of 1000 concurrent clients.

FAQ

Can raw Node.js HTTP handle many SSE connections?

Yes. Node.js event loop handles thousands of concurrent connections efficiently. Each SSE connection uses a small amount of memory (~10-50KB).

How does Node.js SSE compare to using Express?

Express adds routing, middleware, and convenience. Raw HTTP gives more control and less overhead. For production, Express is recommended for its ecosystem.

Should I use HTTP/2 for Node.js SSE?

Yes, if you expect many concurrent connections. HTTP/2 eliminates the 6-connection-per-host limit and multiplexes streams over one TCP connection.

How do I prevent memory leaks in Node.js SSE?

Always clean up intervals, timers, and client references in req.on('close'). Set reasonable limits on event history and concurrent clients.

Can I use SSE behind an nginx reverse proxy?

Yes. Configure nginx to disable buffering for SSE routes: proxy_buffering off; and add X-Accel-Buffering: no header.

Mini Project: Node.js SSE Notifications

const http = require('http');

const clients = new Map();
let clientId = 0;

const server = http.createServer((req, res) => {
    if (req.url === '/notifications') {
        clientId++;
        const id = clientId;

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

        res.write(`event: connected\ndata: ${JSON.stringify({ clientId: id })}\n\n`);

        clients.set(id, res);
        broadcast('clients', { count: clients.size });

        req.on('close', () => {
            clients.delete(id);
            broadcast('clients', { count: clients.size });
        });

    } else {
        res.writeHead(404);
        res.end();
    }
});

function broadcast(event, data) {
    const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
    for (const [id, client] of clients) {
        try {
            client.write(message);
        } catch (e) {
            clients.delete(id);
        }
    }
}

// Simulate notifications
setInterval(() => {
    broadcast('notification', {
        id: Date.now(),
        title: 'New Event',
        message: `Event at ${new Date().toLocaleTimeString()}`,
    });
}, 5000);

server.listen(3000, () => {
    console.log('Notification SSE on :3000/notifications');
});

What's Next

Now that you understand SSE with raw Node.js, explore event types and custom events, then learn about auto-reconnection patterns.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro