Taryana
Back to All Technical Articles/High-Scale Real-Time Notification Architecture with NestJS, Redis Pub/Sub, and WebSocket
NestJSRedisWebSocketTypeScriptArchitecture

High-Scale Real-Time Notification Architecture with NestJS, Redis Pub/Sub, and WebSocket

Lessons from real production war stories handling polling spikes and WebSocket drops during a flash sale: how a decoupled push architecture with NestJS and Redis Pub/Sub eliminated 98% of database load.

Taryana•2026-09-20•10 min read

1. Why Real-Time Notifications Frequently Become Your System's Biggest Point of Failure

Have you ever endured that heart-stopping moment where an apparently straightforward feature—like instant order status alerts or chat notifications—suddenly brings your entire production backend to its knees during a major promotional launch?

That exact ordeal is what my engineering team and I encountered when we first rolled out a live order tracking feature. In our quiet staging environments, everything had operated smoothly. But the moment we deployed to production with tens of thousands of active users, our naive architectural shortcuts ignited a cascade of critical failures:

  • Database Exhaustion: Mobile and web clients were configured to poll the order status endpoint every 2 seconds. With 50,000 active shoppers, our backend was battered by 25,000 merely checking for status changes that had not yet occurred! Primary database CPU spiked to 98%, stalling checkout transactions across the platform.
  • The "Client " Disaster: To reduce load, we initially tried a quick shortcut: frontends fired a REST API mutation, and then manually triggered a client-side socket emit. On spotty cellular networks, the HTTP call cleared but the socket emit dropped. Users had their credit cards charged without ever receiving confirmation banners, sparking panic in our customer support queues.
  • TCP Memory Isolation in Clusters: When our WebSocket gateway scaled across 10 , users connected to A never received live notifications when the order was fulfilled on B!

From these incidents, we formulated an unshakeable architectural rule: never entrust real-time distributed orchestration to client devices, and never depend on a single server node's local memory.

Executive Summary & Core Insight

Solution Blueprint: Decoupled Distributed Push Architecture To permanently eliminate empty loops and guarantee sub-millisecond real-time push deliveries across all cluster nodes, we engineered a 3-pillar defense:

  1. 1Single-Hit Ingestion: Clients execute a single, standard HTTP REST mutation (POST /api/v1/orders). Frontends never manage manual WebSocket emit coordination.
  2. 2Redis Event Backbone: Once the transaction is validated and safely committed to PostgreSQL, the backend broadcasts an event payload onto an Redis channel in sub-millisecond time (<1ms).
  3. 3Horizontally Scaled WebSocket Gateways: WebSocket gateway subscribe to Redis channels and push frames directly to connected client TCP sockets without touching the primary database.
Network Flow: Real-Time Notification & Redis Pub/SubNestJS + Redis + WebSocket
Pinch touchpad / scroll untuk zoom • Klik-drag untuk geser (kiri, kanan, atas, bawah)
Sender ClientSingle HTTP HitPOST /api/v1/ordersNestJS REST APIController & ServiceStateless Ingestion200 OK ReturnedPUBLISHREDIS BUSchannel: live_notifFan-out < 1ms2 Subscriber PodsWS Pod 1 (NestJS)SUBSCRIBE Activeserver.emit()WS Pod 2 (NestJS)SUBSCRIBE Activeserver.emit()Web ClientMobile App
Client Ingestion:1x REST API Hit
Message Bus:Redis Pub/Sub
WebSocket Cluster:Multi-Pod NestJS
Delivery Latency:< 5 ms Push

Klien hanya melakukan satu kali request HTTP ke REST API NestJS. Event di-publish ke Redis Pub/Sub, lalu di-broadcast serentak oleh seluruh pod WebSocket Gateway ke klien aktif.


2. Notification Topology & Architectural Comparison Matrix

Before walking through the code implementation, let us examine how traditional naive approaches compare to our decoupled push architecture:

Engineering Dimension (HTTP GET Intervals)Client (REST + Frontend Emit)Decoupled Push (NestJS + Redis )
Database Query LoadExtreme (Millions of empty queries/min)Moderate (Only on checkout mutations)Minimal (Only genuine business mutations)
Notification LatencyHigh (Bounded by 2-5s intervals)Fast but prone to network dropsInstant Sub-Millisecond (4-8ms via push TCP)
Data Consistency & Vulnerable to stale client statesSevere (Socket disconnects drop alerts)100% Consistent (Push follows DB commit)
Easy but overburdens databasesCompletely broken across isolated Flawless (Unified by Redis )
Security & Spoofing VulnerabilityLowHigh (Clients can spoof emit payloads)Zero (Events minted exclusively by backend)

3. Three Critical Problems Behind Traditional Notification Failures

Problem 1: The Short Polling Trap Draining Connection Pools

When client applications poll on a 2-second interval:

  • 50,000 active users generate 25,000 () of database lookups.
  • Primary PostgreSQL connection pools are drained (), query latencies jump from 15ms to 12,000ms, and paying customers encounter 500 error screens.

Solution 1: Shifting from Pull to Event-Driven Push

By shifting to persistent WebSockets bridged via Redis, the 25,000 storm drops immediately to 0 . TCP connections stay open with negligible overhead, and data flows only when real business events happen.


Problem 2: The Client Dual-Hit Anti-Pattern

When developers attempt a quick workaround:

TypeScript CRDT Reference3 baris kode
> // ANTI-PATTERN: Forcing client devices to orchestrate distributed events! | await axios.post('/api/v1/orders', payload); ...Klik untuk melihat implementasi lengkap →

If a mobile user traverses a subway tunnel between line 1 and line 2, the HTTP order clears, but the socket emit is lost forever. Furthermore, malicious actors can reverse-engineer the client script to spam fake notification events into other users' socket rooms.

Solution 2: Single-Hit Stateless REST Ingestion

Clients issue a single HTTP request. The backend securely authorizes the session, writes the transaction to PostgreSQL, and internally publishes the event to the distributed notification bus.


Problem 3: TCP Connection Memory Isolation Across Kubernetes Pods

In a containerized environment:

  • User A connects to WebSocket Gateway on 1.
  • User B connects to WebSocket Gateway on 2.
  • User A triggers an action handled by API 3. If 3 attempts to broadcast the event solely using its local Node.js memory, User B on 2 will never receive the alert because User B's TCP socket lives in 2's process memory.

Solution 3: Redis Pub/Sub as the Distributed Event Bus

Redis acts as the shared nervous system across all . 3 publishes one message to Redis, and all WebSocket simultaneously receive the event and push it out to their locally connected sockets.


4. The Flash Sale Incident: When 120,000 Concurrent Users Melted Our Database

Last quarter, our platform launched a flash sale. Within the first five minutes, 120,000 eager shoppers rushed into the catalog.

Because our legacy codebase still relied on a mixture of and client socket emits:

  1. 1Saturated mobile networks caused 14% of client socket requests to timeout. Thousands of shoppers assumed their checkout had failed, despite money being deducted from their balances.
  2. 2Panic-driven page refreshes ignited a 45,000 that overwhelmed our PostgreSQL cluster.
  3. 3API and socket starved each other of CPU until the entire infrastructure crashed for 25 minutes.

That catastrophic incident forced us to completely reconstruct our notification pipeline around the decoupled NestJS and Redis architecture detailed below.


5. Production NestJS Implementation (TypeScript)

Here is the modular, type-safe architecture we deployed to production across our Kubernetes clusters:

1. Dedicated Redis Pub/Sub Service

[!TIP]

The ioredis Connection Rule:

Once an ioredis client issues SUBSCRIBE, that connection is strictly locked in listening mode and rejects write commands like PUBLISH or SET. You must maintain dedicated instances for publisherClient and subscriberClient.

TypeScript CRDT Reference52 baris kode
> // src/modules/redis/redis-pubsub.service.ts | import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from "@nestjs/common"; ...Klik untuk melihat implementasi lengkap →

2. Order Controller & Service (Stateless REST Ingestion)

Clients trigger this single HTTP endpoint without managing any socket connections from the frontend:

TypeScript CRDT Reference21 baris kode
> // src/modules/order/order.controller.ts | import { Controller, Post, Body, HttpCode, HttpStatus } from "@nestjs/common"; ...Klik untuk melihat implementasi lengkap →
TypeScript CRDT Reference42 baris kode
> // src/modules/order/order.service.ts | import { Injectable, Logger } from "@nestjs/common"; ...Klik untuk melihat implementasi lengkap →

3. NestJS WebSocket Gateway (Distributed Pod Broadcaster)

TypeScript CRDT Reference51 baris kode
> // src/modules/notification/notification.gateway.ts | import { ...Klik untuk melihat implementasi lengkap →

6. Real Production Metrics: Before vs After

Following our migration to this Decoupled Push architecture in production, our metrics told a dramatic story:

Architecture MetricBefore ( & )After (NestJS + Redis + WS)Production Impact
Notification Delivery Latency2,000 ms (waiting for next poll cycle)4.1 ms (sub-frame WebSocket push)487x Faster (Feels instant to users)
Database Queries / Minute1,500,000 queries / min24,000 queries / min98.4% Load Reduction (DB remains calm)
HTTP Ingestion Throughput1,200 (choked by connections)18,500+ 15x Capacity Expansion
Notification Loss Rate4.8% (network drops & races)0.00% (Guaranteed backend dispatch)Zero data inconsistencies
API CPU Utilization85% - 94% (Constant alert state)16% - 22%75% Infrastructure Cost Savings

7. 4 Core Engineering Lessons from the Trenches

  1. 1Client Single-Hit Principle: Never burden client applications with distributed notification orchestration. Clients execute one standard HTTP mutation, and the backend handles event broadcasting.
  2. 2Decouple APIs from Sockets: Separate REST API from WebSocket Gateway . REST can be freely redeployed or autoscaled without severing tens of thousands of active socket sessions.
  3. 3Always Isolate ioredis Publisher and Subscriber Instances: Because Redis protocol locks connections in listening mode during SUBSCRIBE, maintaining dedicated client instances avoids silent write failures.
  4. 4Transient vs Persistent Storage Balance: Utilize Redis for lightweight, memory-efficient real-time delivery, but always store notification histories in your primary database so offline users never miss important updates.
Real-Time Notification Simulator
Memuat lab interaktif...

Ingin bereksplorasi lebih dalam?

Buka seluruh koleksi simulator sistem terdistribusi di Central Sandbox Playground.

Buka Central Sandbox →