This guide teaches you how to build a lightweight real-time data stream using Server-Sent Events (SSE) with a Node.js backend and a React 19 frontend. You will have a working SSE API pushing live updates to your UI by the end — the entire setup takes roughly 90 minutes and requires no WebSocket infrastructure or third-party realtime services.
What You'll Build
- A Node.js Express server that opens persistent SSE connections and pushes typed event streams to connected clients
- A React 19 component that consumes the stream using the native
EventSourceAPI and renders live updates without polling - A heartbeat mechanism that keeps connections alive through proxies and load balancers
- A clean reconnection strategy that recovers from dropped connections automatically
- A production-ready pattern deployable to platforms like Railway, Render, or a Hetzner VPS in 2026
Prerequisites
- Node.js 20+ and pnpm 9+ installed locally
- Basic familiarity with Express and React hooks
- A code editor — VS Code with the ESLint extension works well
- Basic knowledge of HTTP request/response cycles
Why SSE Instead of WebSockets in 2026?
WebSockets are bidirectional. SSE is unidirectional — server to client only. That constraint makes SSE the right tool for dashboards, live notifications, log tails, order status feeds, and AI response streaming.
SSE runs over plain HTTP/1.1 or HTTP/2. It uses native browser reconnection. It does not require sticky sessions on most modern load balancers. For SMBs in Australia and Singapore running lean infrastructure, SSE is significantly easier to operate than a WebSocket server at scale.
SSE also works through corporate proxies that block WebSocket upgrades — a real concern for B2B SaaS products selling into enterprise clients in Canada and the US.
Step 1: Scaffold the Node.js Server
Create a new project and install Express plus CORS support.
mkdir sse-demo && cd sse-demo
pnpm init -y
pnpm add express cors
pnpm add -D typescript @types/express @types/cors ts-node-dev
npx tsc --init
Update tsconfig.json to target ES2022 and set moduleResolution to bundler. This matches the Node 20 module resolution used by most 2026 toolchains.
What does the tsconfig change affect?
Setting moduleResolution: bundler allows TypeScript to resolve ESM-style bare imports without requiring explicit .js extensions in your source files. It eliminates a common class of import errors when mixing CommonJS and ESM in Node 20 projects.
Step 2: Write the SSE Endpoint
Create src/server.ts and add the following:
import express, { Request, Response } from 'express';
import cors from 'cors';
const app = express();
app.use(cors({ origin: 'http://localhost:5173' }));
// Track active clients
const clients = new Set();
app.get('/events', (req: Request, res: Response) => {
// Set SSE headers
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Nginx-Buffering', 'no'); // disable nginx buffering
res.flushHeaders();
clients.add(res);
console.log(`Client connected. Total: ${clients.size}`);
// Send a heartbeat every 15 seconds
const heartbeat = setInterval(() => {
res.write(':heartbeat\
\
');
}, 15_000);
req.on('close', () => {
clearInterval(heartbeat);
clients.delete(res);
console.log(`Client disconnected. Total: ${clients.size}`);
});
});
// Broadcast helper
function broadcast(event: string, data: unknown) {
const payload = `event: ${event}\
data: ${JSON.stringify(data)}\
\
`;
clients.forEach((client) => client.write(payload));
}
// Simulate live data — replace with your real data source
setInterval(() => {
broadcast('metric', {
timestamp: Date.now(),
value: Math.floor(Math.random() * 100),
});
}, 2_000);
app.listen(3001, () => console.log('SSE server on http://localhost:3001'));
Why does the X-Accel-Nginx-Buffering header matter?
Nginx buffers responses by default. Without this header, your SSE events will be held in a buffer and released in bursts rather than streamed individually. Setting X-Accel-Nginx-Buffering: no tells Nginx to pass chunks through immediately. This single line eliminates the most common SSE deployment bug teams hit when moving from local development to production.
Add a start script to package.json:
"scripts": {
"dev": "ts-node-dev --respawn src/server.ts"
}
Run pnpm dev. Visit http://localhost:3001/events in your browser. You should see raw event data streaming in the response body every two seconds.
Step 3: Scaffold the React Frontend
In a separate terminal, create a Vite + React + TypeScript project:
pnpm create vite@latest sse-client -- --template react-ts
cd sse-client && pnpm install
Vite 6 and React 19 are the current stable versions as of June 2026. This template gives you fast HMR and native ESM out of the box.
Step 4: Build the useSSE Hook
Create src/hooks/useSSE.ts. Encapsulating SSE logic in a custom hook keeps your components clean and makes the connection reusable across your app.
import { useEffect, useRef, useState } from 'react';
type SSEOptions = {
url: string;
eventName: string;
withCredentials?: boolean;
};
export function useSSE(options: SSEOptions) {
const { url, eventName, withCredentials = false } = options;
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const sourceRef = useRef(null);
useEffect(() => {
const source = new EventSource(url, { withCredentials });
sourceRef.current = source;
source.addEventListener(eventName, (e: MessageEvent) => {
try {
setData(JSON.parse(e.data) as T);
setError(null);
} catch {
setError('Failed to parse event data');
}
});
source.onerror = () => {
setError('Connection lost — reconnecting...');
// EventSource reconnects automatically after ~3 seconds
};
return () => {
source.close();
};
}, [url, eventName, withCredentials]);
return { data, error };
}
Does EventSource reconnect automatically?
Yes. The browser's native EventSource API retries the connection automatically after approximately three seconds when the connection drops. You can customise the retry delay by sending a retry: 5000 field (in milliseconds) from the server. You do not need a manual reconnection loop for most use cases.
Step 5: Render Live Data in a Component
Open src/App.tsx and replace its contents:
import { useSSE } from './hooks/useSSE';
type Metric = {
timestamp: number;
value: number;
};
export default function App() {
const { data, error } = useSSE({
url: 'http://localhost:3001/events',
eventName: 'metric',
});
return (
Live Metric Stream
{error && {error}
}
{data ? (
<>
Value: {data.value}
Updated: {new Date(data.timestamp).toLocaleTimeString()}
>
) : (
Waiting for data...
)}
);
}
Run pnpm dev inside sse-client. Open http://localhost:5173. The metric value updates every two seconds without any polling or page refresh.
Step 6: Add Named Events and Multiple Channels
Real applications stream multiple event types — order updates, user activity, system alerts. Add a second event type to the server:




