Real-Time Notifications and Presence System Design

#nodejs #notifications #presence #websockets #redis #architecture

Two of the most requested features in modern web applications are the real-time notification bell (the little red dot that appears the moment someone tags you) and the presence indicator (the green dot showing a user is currently online). While these features seem small on the frontend, implementing them reliably across a distributed backend architecture is surprisingly complex.

This article details the architectural patterns for building scalable notification and presence systems using Node.js, WebSockets, and Redis.

Architecting the Real-Time Notification System

A real-time notification system cannot rely solely on WebSockets. If a user is offline when a notification is generated, the WebSocket emit goes into the void. Therefore, a notification system must be a hybrid of a persistent database layer and an ephemeral real-time layer.

1. The Database as the Source of Truth:
When a system event occurs (e.g., User A comments on User B's post), the business logic must first write the notification to a persistent database (PostgreSQL or MongoDB). This ensures that when User B logs in tomorrow, they can fetch their unread notifications via a standard REST API call.

2. The Real-Time Push:
Only after the database write is successful does the system attempt a real-time push. The service layer publishes an event to Redis Pub/Sub: { type: 'NEW_NOTIFICATION', targetUserId: 'B123', payload: {...} }. The Node.js WebSocket servers listen to this channel. If the server holds an active connection for User B, it emits the notification payload directly to User B's browser, turning the bell icon red instantly.

3. Batching and Throttling:
If a user imports 1,000 contacts, you do not want to send 1,000 individual WebSocket events and write 1,000 separate database rows in a single second. Notifications must be queued (using BullMQ). The worker process should aggregate similar notifications ("User A and 12 others liked your post") over a 60-second window before writing to the database and emitting the WebSocket event.

Architecting the Presence System (Who is Online?)

Building a "Who is Online" feature is notoriously difficult in a multi-server environment. You cannot just keep a JavaScript array of onlineUsers in your Node.js server memory, because Server 1 does not know who is connected to Server 2.

1. Redis as the Central Presence Store:
We must use Redis to maintain a global state of online users. When a user establishes a WebSocket connection, the Node.js server executes a Redis command to add them to a Set of online users.

2. The Disconnection Problem (Ghost Users):
If a user closes their browser cleanly, the WebSocket disconnect event fires, and the Node.js server removes the user from the Redis Set. However, if the user's internet drops, or their mobile phone goes to sleep, or the Node.js server crashes abruptly, the disconnect event never fires. The user remains stuck as "online" in Redis forever (a ghost user).

3. The Solution: Redis Keys with TTL (Time-To-Live):
Instead of a simple Set, we use individual Redis keys with a short expiration time (e.g., 60 seconds).

  • On Connection: SET user:online:123 "true" EX 60
  • The Heartbeat: The client's browser must send a WebSocket "ping" every 30 seconds. When the Node.js server receives this ping, it resets the Redis key expiration back to 60 seconds (EXPIRE user:online:123 60).
  • Disconnection: If the user's internet drops, the pings stop. Redis automatically deletes the key after 60 seconds. The user is now officially "offline."

Broadcasting Presence Changes

Knowing someone is online in Redis is only half the battle; the UI needs to update when their status changes.

If you have a chat app with 5,000 active users, and you broadcast a "User X went offline" event to all 5,000 users every time someone closes their laptop, you will choke the network bandwidth. Presence updates must be scoped.

  • Room-Based Presence: Only broadcast presence changes to users who care. If User A joins a specific project dashboard (Room: project:456), the server emits User A's presence only to other users currently in project:456.
  • Subscription Lists: For a chat contact list, the client sends an array of User IDs they want to observe. The server places the client in specific Redis Pub/Sub channels for those target users. When a target user connects/disconnects, the event is only pushed to clients explicitly subscribed to that user's status.

Presence and notification systems are fundamental features of modern SaaS, but they introduce significant architectural complexity. If you are looking to build or scale real-time features in your application, visit my Node.js development services page to discuss your project.


Prakash Tank

Prakash Tank

Full-Stack Architect & Tech Enthusiast. Passionate about building scalable applications and sharing knowledge with the community.