L
Node.js • Literally Web ID

Membangun Real-time Notification Pipeline dengan Node.js, Redis Pub/Sub, dan WebSocket

⏱️ 18 menit baca • 📅 20 Oct 2026 • 👁️ 2 views
Membangun Real-time Notification Pipeline dengan Node.js, Redis Pub/Sub, dan WebSocket

1. Konteks Industri & Analisis Akar Masalah

Di era digital modern, ekspektasi pengguna terhadap aplikasi web telah bergeser drastis. Halaman web statis dan siklus request-response HTTP tradisional (polling) kini dianggap usang untuk platform interaktif seperti aplikasi finansial real-time, e-commerce flash sale, dashboard monitoring infrastruktur, hingga platform kolaborasi. Pengguna menuntut pembaruan data instan tanpa harus melakukan refresh manual.

Namun, mengimplementasikan sistem real-time berskala besar menghadirkan tantangan arsitektural yang masif. Pendekatan naive—seperti membuka ribuan koneksi WebSocket langsung ke satu instance aplikasi Node.js—akan dengan cepat menghabiskan memori RAM, membebani CPU dengan event loop blocking, dan menciptakan titik kegagalan tunggal (single point of failure). Ketika aplikasi harus diskalakan secara horizontal (horizontal scaling) dengan menaikkan jumlah instance server menjadi 5 atau 10 container di balik sebuah load balancer, masalah baru muncul: bagaimana sebuah pesan yang dikirim oleh pengguna di Server A bisa sampai ke klien yang terhubung di Server B?

Kondisi Sebelum vs Sesudah Penerapan

  • Sebelum (Arsitektur Naive / Polling / Single-Node WebSocket):

    • Latensi Tinggi: Klien melakukan HTTP polling setiap beberapa detik, menciptakan beban overhead jaringan (header HTTP redundancy) yang masif pada web server (Nginx/Apache).
    • Tidak Scalable: Koneksi WebSocket terisolasi pada satu instance server. Klien yang terhubung ke Node.js Instance #1 tidak dapat menerima broadcast pesan yang di-trigger dari Node.js Instance #2.
    • Resource Exhaustion: Sering terjadi memory leak pada heap V8 Node.js akibat pengelolaan state koneksi yang buruk, menyebabkan crash massal saat trafik melonjak.
  • Sesudah (Arsitektur Pipeline Redis Pub/Sub + WebSocket Cluster):

    • Latensi Rendah & Efisien: Koneksi persisten via WebSocket menjaga saluran komunikasi tetap terbuka dengan overhead minimal.
    • Skalabilitas Horizontal Tak Terbatas: Redis Cluster bertindak sebagai message broker terpusat yang mendistribusikan event lintas instance Node.js secara instan menggunakan pola Publish/Subscribe.
    • Ketahanan Tinggi (Resiliency): Mekanisme heartbeat, automatic reconnection, dan graceful shutdown memastikan sistem tetap stabil meskipun terjadi kegagalan jaringan atau rolling update pada infrastruktur.

2. Konsep Inti & Deep Dive Arsitektur

Arsitektur real-time notification pipeline yang tangguh memadukan beberapa komponen infrastruktur kunci: Client (Browser/Mobile), Reverse Proxy (Nginx), Cluster Node.js (WebSocket Server), dan Redis In-Memory Data Store (Pub/Sub Engine).

Alur Eksekusi Data (Data Flow)

  1. Inisiasi Koneksi: Klien melakukan handshake HTTP ke Nginx, yang kemudian di-upgrade menjadi protokol WebSocket dan di-load balancing ke salah satu instance Node.js (misal: Node-App-1).
  2. Penyimpanan State: Instance Node.js mencatat pemetaan socketId ke userId di dalam struktur data lokal (atau Redis untuk multi-cluster tracking).
  3. Trigger Event (Publisher): Sebuah layanan backend (misalnya API transaksi berbasis Go atau PHP) mempublikasikan event baru ke Redis menggunakan perintah PUBLISH channel_name payload.
  4. Distribusi Redis Pub/Sub: Redis menerima pesan tersebut dan secara instan menyiarkannya ke seluruh subscriber yang terdaftar pada channel tersebut, termasuk semua instance Node.js yang mendengarkan (Node-App-1, Node-App-2, Node-App-3).
  5. Pengiriman ke Klien: Setiap instance Node.js menerima pesan dari Redis, memeriksa apakah target klien terhubung secara lokal pada instance tersebut, dan jika ya, mengirimkan payload melalui socket WebSocket yang sesuai.
+--------+      WebSocket      +-------+       Load Balance      +-------------+
| Client | ------------------> | Nginx | --------------------> | Node.js #1  |
+--------+                     +-------+                       +-------------+
                                                                      |
                                                                 Redis Pub/Sub
                                                                 (Message Broker)
                                                                      |
+--------+      WebSocket      +-------+       Load Balance      +-------------+
| Client | ------------------> | Nginx | --------------------> | Node.js #2  |
+--------+                     +-------+                       +-------------+

3. Perbandingan Komparatif / Analisis Pendekatan

Untuk memilih arsitektur real-time yang tepat, mari kita bandingkan tiga pendekatan populer dalam dunia enterprise:

Parameter Komparasi HTTP Short/Long Polling Server-Sent Events (SSE) WebSocket + Redis Pub/Sub
Arah Komunikasi Unidirectional (Client-driven) Unidirectional (Server-to-Client) Bidirectional (Full-duplex)
Konsumsi CPU & RAM Sangat Tinggi (Banyak proses HTTP handshake) Sedang (Koneksi HTTP persisten) Rendah (Satu koneksi TCP persisten)
Latensi Tinggi (> 1 - 5 detik) Rendah (< 50 ms) Sangat Rendah (< 10 ms)
Skalabilitas Horizontal Mudah (Standard Load Balancing) Sedikit kompleks (Butuh sticky sessions) Membutuhkan Message Broker (Redis/Kafka)
Dukungan Protokol HTTP/1.1 HTTP/1.1 & HTTP/2 TCP / WebSocket Protocol
Kasus Penggunaan Ideal Form submission, simple dashboard Live ticker, stock price feed Chat app, collaborative tools, notifications

[!WARNING] Jangan menggunakan HTTP Long Polling untuk sistem notifikasi skala besar (> 10.000 concurrent users) karena akan menyebabkan file descriptor exhaustion pada web server Anda.


4. Prasyarat Sistem & Persiapan Lingkungan

Sebelum mulai menulis kode, pastikan infrastruktur sistem operasi dan dependensi perangkat lunak telah disiapkan dengan benar pada environment Linux (Ubuntu 22.04 LTS direkomendasikan).

Spesifikasi & Dependensi Minimum

  • Node.js: Versi 20.x LTS atau lebih baru.
  • Redis Server: Versi 7.x dengan konfigurasi Cluster atau Sentinel untuk High Availability.
  • Nginx: Versi 1.18+ (berfungsi sebagai Reverse Proxy dan SSL Termination).
  • System Limits: Konfigurasi nofile (maximum open files) pada sistem operasi harus ditingkatkan untuk menampung puluhan ribu koneksi WebSocket.

Konfigurasi System Limits (/etc/security/limits.conf)

* soft nofile 65535
* hard nofile 65535
root soft nofile 65535
root hard nofile 65535

Konfigurasi Kernel (/etc/sysctl.conf)

fs.file-max = 2097152
net.core.somaxconn = 65535
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_tw_reuse = 1

Terapkan perubahan kernel di atas dengan perintah terminal:

sudo sysctl -p

5. Panduan Implementasi Langkah demi Langkah

Langkah 1: Inisialisasi Project Node.js

Buat direktori kerja baru dan instal dependensi yang diperlukan (ws, ioredis, dotenv).

mkdir realtime-pipeline && cd realtime-pipeline
npm init -y
npm install ws ioredis dotenv

Langkah 2: Konfigurasi Nginx Reverse Proxy (/etc/nginx/sites-available/realtime)

Nginx harus dikonfigurasi secara khusus agar mendukung connection upgrade header untuk protokol WebSocket dan mencegah timeout prematur.

upstream websocket_backend {
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
}

server {
    listen 80;
    server_name notification.internal.system;

    location / {
        proxy_pass http://websocket_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        
        # Timeout configurations for long-lived WebSocket connections
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
    }
}

Langkah 3: Implementasi Server Node.js (server.js)

Berikut adalah kode implementasi backend Node.js lengkap yang mengintegrasikan WebSocket server (ws) dengan Redis Pub/Sub (ioredis).

require('dotenv').config();
const { WebSocketServer } = require('ws');
const Redis = require('ioredis');

const PORT = process.env.PORT || 3001;
const REDIS_URL = process.env.REDIS_URL || 'redis://127.0.0.1:6379';

// Inisialisasi Redis Publisher & Subscriber
// Redis memerlukan koneksi terpisah untuk mode Pub/Sub
const pub = new Redis(REDIS_URL);
const sub = new Redis(REDIS_URL);

// Inisialisasi WebSocket Server
const wss = new WebSocketServer({ port: PORT });

// Map untuk menyimpan koneksi aktif berdasarkan userId: { userId: WebSocket }
const clients = new Map();

console.log(`[INFRA] WebSocket Server berjalan pada port ${PORT}`);

// Subscribe ke channel Redis global untuk notifikasi
sub.subscribe('notifications', (err, count) => {
    if (err) {
        console.error('[REDIS ERROR] Gagal melakukan subscribe ke channel:', err);
        process.exit(1);
    }
    console.log(`[REDIS] Berhasil subscribe ke ${count} channel. Mendengarkan event...`);
});

// Tangani pesan yang masuk dari Redis Pub/Sub
sub.on('message', (channel, message) => {
    if (channel === 'notifications') {
        try {
            const data = JSON.parse(message);
            const { targetUserId, payload } = data;

            // Kirim pesan ke klien jika terhubung di instance Node.js ini
            if (clients.has(targetUserId)) {
                const wsClient = clients.get(targetUserId);
                if (wsClient.readyState === wsClient.OPEN) {
                    wsClient.send(JSON.stringify(payload));
                    console.log(`[NOTIFICATION] Berhasil dikirim ke user: ${targetUserId}`);
                }
            }
        } catch (parseError) {
            console.error('[PARSE ERROR] Format pesan Redis tidak valid:', parseError);
        }
    }
});

// Manajemen Koneksi WebSocket
wss.on('connection', (ws, req) => {
    // Contoh sederhana: Ekstraksi userId dari query parameter (misal: ws://domain.com?userId=12345)
    const urlParams = new URLSearchParams(req.url.split('?')[1]);
    const userId = urlParams.get('userId');

    if (!userId) {
        ws.close(4001, 'Unauthorized: userId diperlukan');
        return;
    }

    // Simpan referensi koneksi
    clients.set(userId, ws);
    console.log(`[WS CONNECT] User terhubung: ${userId}. Total koneksi lokal: ${clients.size}`);

    // Kirim ping berkala (Heartbeat) untuk mendeteksi koneksi terputus
    ws.isAlive = true;
    ws.on('pong', () => {
        ws.isAlive = true;
    });

    // Tangani pesan masuk dari klien (jika ada komunikasi dua arah)
    ws.on('message', (message) => {
        console.log(`[WS MESSAGE] Menerima pesan dari ${userId}:`, message.toString());
    });

    // Pembersihan saat koneksi terputus
    ws.on('close', () => {
        clients.delete(userId);
        console.log(`[WS DISCONNECT] User terputus: ${userId}. Sisa koneksi lokal: ${clients.size}`);
    });

    ws.on('error', (error) => {
        console.error(`[WS ERROR] Error pada socket user ${userId}:`, error);
    });
});

// Interval untuk pengecekan Heartbeat (Ping/Pong)
const interval = setInterval(() => {
    wss.clients.forEach((ws) => {
        if (ws.isAlive === false) return ws.terminate();
        ws.isAlive = false;
        ws.ping();
    });
}, 30000);

wss.on('close', () => {
    clearInterval(interval);
});

// Graceful Shutdown
process.on('SIGTERM', () => {
    console.log('[SHUTDOWN] Menghentikan server secara aman...');
    wss.close(() => {
        pub.quit();
        sub.quit();
        process.exit(0);
    });
});

6. Bedah Kode & Penjelasan Detail Baris Kunci

  • const sub = new Redis(REDIS_URL);: Mengapa kita membuat instance Redis baru khusus untuk sub? Berdasarkan spesifikasi klien Redis (seperti ioredis), ketika sebuah koneksi masuk ke mode subscriber (SUBSCRIBE), koneksi tersebut tidak dapat digunakan untuk mengeksekusi perintah database lain (seperti GET, SET, atau PUBLISH). Oleh karena itu, arsitektur dual-connection (pub dan sub) wajib diterapkan.
  • ws.on('pong', ...) & Interval Ping 30 Detik: Mekanisme ini krusial untuk mendeteksi kondisi half-open connections (misalnya ketika kabel jaringan klien terputus secara tiba-tiba tanpa mengirimkan paket TCP FIN). Jika klien tidak merespons ping server dalam waktu yang ditentukan, socket akan dihentikan (terminate()) secara paksa untuk membebaskan memori server.
  • const clients = new Map();: Penggunaan struktur data Map bawaan JavaScript jauh lebih efisien dibandingkan Plain Object ({}) dalam hal operasi pencarian (lookup) dan penghapusan (deletion) frekuensi tinggi dengan ribuan kunci dinamis.

7. Skenario Produksi & Studi Kasus Dunia Nyata

Studi Kasus: Sistem Notifikasi Finansial (100.000 Konkurensi)

Dalam platform perbankan digital, saat terjadi transaksi masuk, API core banking (yang ditulis dalam bahasa Go) akan mengeksekusi perintah Redis:

PUBLISH notifications '{"targetUserId":"usr_998877","payload":{"type":"CREDIT","amount":1500000,"currency":"IDR"}}'

Melalui Redis Cluster, pesan ini disebarkan secara instan ke seluruh pod Node.js di Kubernetes. Kubernetes Horizontal Pod Autoscaler (HPA) dikonfigurasi berdasarkan metrik konsumsi CPU Node.js. Jika trafik melonjak hingga 500.000 koneksi aktif, HPA otomatis menambah replica container Node.js dari 3 menjadi 15 instance tanpa kehilangan pesan apa pun, karena Redis bertindak sebagai buffer penyangga yang andal.


8. Common Pitfalls, Anti-Patterns & Solusi

  1. Anti-Pattern: Menyimpan State Koneksi di Database Relasional
    • Masalah: Melakukan query ke MySQL/PostgreSQL setiap kali ada pesan masuk untuk mencari IP/Server tempat klien terhubung akan melumpuhkan database.
    • Solusi: Gunakan Redis sebagai in-memory lookup store atau andalkan mekanisme broadcast channel Redis Pub/Sub seperti yang dicontohkan di atas.
  2. Pitfall: Lupa Mengatur proxy_read_timeout di Nginx
    • Masalah: Koneksi WebSocket terputus secara otomatis setiap 60 detik oleh Nginx.
    • Solusi: Selalu tambahkan proxy_read_timeout 86400s; pada blok konfigurasi Nginx location.
  3. Pitfall: Memory Leak pada Event Listener
    • Masalah: Menambahkan event listener baru pada objek ws setiap kali ada pesan masuk tanpa menghapusnya.
    • Solusi: Gunakan metode sekali pakai atau pastikan pembersihan listener dilakukan secara eksplisit pada event close.

9. Benchmark, Metrik Kinerja & Tuning Performa

Untuk menguji kemampuan beban pipeline yang telah dibangun, Anda dapat menggunakan alat beban (load testing tool) seperti Artillery.io atau Autocannon khusus untuk protokol WebSocket.

Contoh Perintah Pengujian dengan Artillery

Buat file skrip pengujian load-test.yml:

config:
  target: "ws://127.0.0.1:3001"
  phases:
    - duration: 60
      arrivalRate: 50
      rampTo: 500
      name: "Ramp-up load"
scenarios:
  - engine: "ws"
    flow:
      - connect: "?userId={{ $randomString() }}"
      - send: "Ping from load tester"
      - think: 10

Jalankan pengujian:

artillery run load-test.yml

Metrik Kinerja Utama yang Harus Dipantau:

  • Event Loop Lag (Node.js): Harus dijaga di bawah 10ms. Jika lag meningkat, artinya terjadi operasi sinkron yang memblokir event loop.
  • RSS Memory Usage: Pastikan tidak terjadi lonjakan memori kontinu (memory leak) per koneksi WebSocket.
  • Redis Latency (redis-cli --latency): Latensi respons Redis harus berada di bawah 1ms.

10. Checklist Penerapan Produksi & Kesimpulan

Sebelum meluncurkan pipeline ini ke lingkungan produksi (production), pastikan Anda telah mencentang seluruh daftar periksa berikut:

  • Konfigurasi parameter OS nofile dan somaxconn telah diterapkan pada server.
  • Nginx telah dikonfigurasi dengan SSL/TLS aktif (wss://) dan timeout yang disesuaikan.
  • Instance Redis telah dilengkapi dengan mekanisme Persistence (RDB/AOF) atau menggunakan layanan Redis terkelola (AWS ElastiCache / Redis Enterprise).
  • Implementasi Graceful Shutdown pada aplikasi Node.js telah diuji untuk mencegah dropped connections saat proses rolling deployment.
  • Monitoring metrik (Prometheus + Grafana) untuk CPU, Memory, dan Active WebSocket Connections telah diaktifkan.

Dengan mengikuti panduan komprehensif ini, Anda kini memiliki fondasi arsitektur real-time notification pipeline tingkat enterprise yang sangat cepat, skalabel, dan tahan banting terhadap lonjakan trafik ekstrem.

🏷️ Tags: #Redis #Websocket #System Architecture #Backend Engineering #Node.js