js

Server-Sent Events Guide: Build Real-Time Notifications with Express.js and Redis for Scalable Apps

Learn to build scalable real-time notifications with Server-Sent Events, Express.js & Redis. Complete guide with authentication, error handling & production tips.

Server-Sent Events Guide: Build Real-Time Notifications with Express.js and Redis for Scalable Apps

Let me tell you about a problem I used to face all the time. I’d build an application, and users would ask, “Why don’t I see my notifications right away?” The page felt dead, static. Refreshing constantly wasn’t the answer. That’s what pushed me to explore a better way to keep users informed instantly, without complexity. This journey led me to a powerful, often overlooked technology perfect for one-way updates.

Think about a live sports score, a package tracker, or a news feed. The server has new information, and your browser needs to show it. That’s the core problem. I found that Server-Sent Events, or SSE, is one of the most straightforward solutions for this specific job. It’s built right into your browser and uses a standard HTTP connection that stays open. The server can just… push.

Why not use WebSockets for everything? Great question. WebSockets are fantastic for two-way chat, like a support window where you send and receive constantly. But they are a more complex tool. For simple notifications—where the server talks and the client listens—SSE is simpler. It handles reconnection automatically if the network drops, and it works easily with existing web infrastructure. Have you ever watched a package tracker update in real time? That feeling of live information is what we can build.

Let’s start with the basics on the server. We’ll use Express.js, a framework I find very approachable. The key is setting the correct headers so the browser knows this is an event stream.

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

  // Send a test message every 10 seconds
  const intervalId = setInterval(() => {
    const message = `data: ${JSON.stringify({ update: 'Server time is ' + new Date().toISOString() })}\n\n`;
    res.write(message);
  }, 10000);

  // Clean up when the client closes the connection
  req.on('close', () => {
    clearInterval(intervalId);
    console.log('Client disconnected');
  });
});

This code creates a route. When a client connects, it sends a JSON message every ten seconds. The format is crucial: data: followed by the content and two newlines (\n\n). That’s the SSE protocol. But what happens if you have 10,000 users? A single server holding all those open connections can struggle. This is where we need to think about scale.

The challenge is sharing messages across multiple servers. Imagine User A connects to Server 1, but their notification is triggered from Server 2. Server 2 needs a way to tell Server 1 to send the message. This is a classic job for a message broker. I use Redis for this because it’s fast and has a perfect feature called Pub/Sub (Publish/Subscribe).

Here’s how it changes our setup. Each server instance subscribes to a common Redis channel. When something needs to be notified, we publish to that channel.

const Redis = require('ioredis');
const publisher = new Redis();
const subscriber = new Redis();

// Store active connections on this server
const clients = new Map();

app.get('/stream', (req, res) => {
  // ... (set SSE headers as before)
  const clientId = Date.now();
  clients.set(clientId, res);

  // Listen for messages from Redis
  subscriber.on('message', (channel, message) => {
    const dataMsg = `data: ${message}\n\n`;
    res.write(dataMsg);
  });
  subscriber.subscribe('notifications');

  req.on('close', () => {
    clients.delete(clientId);
    console.log(`Client ${clientId} left`);
  });
});

// An API endpoint to trigger a notification
app.post('/alert', async (req, res) => {
  const alert = { priority: 'high', text: 'System update scheduled' };
  // Publish to Redis, all subscribed servers will get it
  await publisher.publish('notifications', JSON.stringify(alert));
  res.send({ status: 'Notification sent' });
});

Now, the /alert endpoint can be called from anywhere. It publishes a message to Redis. Every server instance listening on the 'notifications' channel receives it and forwards it to its connected clients. We’ve just made our system stateless and scalable. But wait, how do we know which user should get which message? We can’t send every alert to every person.

This is where authentication and channels come in. When a user connects, we can verify their identity (e.g., using a session cookie or token) and subscribe them to a private channel, like user-123-notifications. The backend service that triggers the alert publishes to that specific channel.

app.get('/my-stream', authenticateUser, (req, res) => {
  // After auth, we have req.user.id
  const userChannel = `user-${req.user.id}`;

  // Subscribe to this user's personal channel
  subscriber.subscribe(userChannel);
  const listener = (channel, msg) => {
    if(channel === userChannel) {
      res.write(`data: ${msg}\n\n`);
    }
  };
  subscriber.on('message', listener);

  req.on('close', () => {
    subscriber.removeListener('message', listener);
    subscriber.unsubscribe(userChannel);
  });
});

The system becomes targeted and secure. Each user gets their own stream. On the frontend, using this is beautifully simple. The browser’s EventSource API handles the connection, reconnection, and message parsing for you.

// In your browser's JavaScript
const eventSource = new EventSource('/my-stream');

eventSource.onmessage = (event) => {
  const notification = JSON.parse(event.data);
  console.log('New alert:', notification);
  // Update your UI here
};

eventSource.addEventListener('system', (event) => {
  // You can also listen for specific event types
  console.log('System event:', event.data);
});

eventSource.onerror = (error) => {
  console.error('EventSource failed:', error);
  // The browser will automatically try to reconnect
};

Putting it all together, you have a robust pipeline. The client opens a simple connection. Your Express server manages authenticated sessions. Redis acts as the nervous system, routing messages to the correct server and user. The result is live, immediate notifications without the user lifting a finger.

Building this changed how I see real-time features. It doesn’t have to be daunting. By combining the right, simple tools—HTTP for transport, Express for structure, Redis for distribution—you can create an experience that feels alive and responsive. What kind of live updates could make your application feel more dynamic?

I hope this walkthrough helps you add that instant, connected feel to your own projects. If you found this guide useful, please share it with other developers who might be facing the same static-page struggle. Have you implemented something similar? I’d love to hear about your experiences or answer any questions in the comments below

Keywords: server-sent events, express.js SSE, redis real-time notifications, SSE implementation guide, WebSocket vs SSE, real-time notifications javascript, express server-sent events, redis pub sub nodejs, SSE authentication express, scalable notifications system



Similar Posts
Blog Image
Building Type-Safe Event-Driven Architecture with TypeScript NestJS and RabbitMQ Complete Guide

Learn to build scalable event-driven microservices with TypeScript, NestJS & RabbitMQ. Master type-safe event handling, message brokers & resilient architecture patterns.

Blog Image
Build High-Performance Rate Limiting Middleware with Redis and Node.js: Complete Tutorial

Learn to build scalable rate limiting middleware with Redis & Node.js. Master token bucket, sliding window algorithms for high-performance API protection.

Blog Image
How to Build Multi-Tenant SaaS Architecture with NestJS, Prisma and PostgreSQL

Learn to build scalable multi-tenant SaaS architecture with NestJS, Prisma & PostgreSQL. Master tenant isolation, dynamic connections, and security best practices.

Blog Image
Complete Guide to Next.js Prisma Integration: Build Type-Safe Full-Stack Apps in 2024

Learn how to integrate Next.js with Prisma ORM for powerful full-stack development. Build type-safe web apps with React frontend and modern database toolkit.

Blog Image
Complete Guide to Integrating Next.js with Prisma ORM for Type-Safe Full-Stack Development

Learn how to integrate Next.js with Prisma ORM for powerful full-stack applications. Get step-by-step guidance on setup, type safety, and database operations.

Blog Image
Build Scalable Real-time Apps with Socket.io Redis Adapter and TypeScript in 2024

Learn to build scalable real-time apps with Socket.io, Redis adapter & TypeScript. Master chat rooms, authentication, scaling & production deployment.