# Membangun Real-time Notification System dengan Node.js dan Redis Pub/Sub

> Diterbitkan oleh Literally Web ID pada 21 Oct 2026
> Sumber: https://literally.web.id/articles/membangun-real-time-notification-system-dengan-nodejs-dan-redis-pubsub

---

## 1. Alarm Tengah Malam: Saat Layanan Produksi Mulai Tumbang

Pukul 02:14 WIB. PagerDuty saya menjerit melengking, membangunkan saya dari tidur lelap. Layar ponsel langsung dibanjiri notifikasi merah dari sistem monitoring Grafana: *CPU Usage* node backend menyentuh 98%, *Memory Leak* terdeteksi di container Node.js, dan yang paling mengerikan—*Database Connection Pool* MySQL meluber hingga batas maksimal, membuat ribuan request HTTP berakhir dengan status kode `504 Gateway Timeout` di balik load balancer Nginx.

Malam itu, kampanye flash sale produk andalan baru saja dimulai oleh tim marketing. Ribuan pengguna yang membuka halaman web mendadak mengalami kegagalan menerima pembaruan status pesanan secara *real-time*. Arsitektur yang awalnya dirancang mulus mendadak menjadi tumpukan besi tua digital yang sekarat. Masalah utamanya klasik namun mematikan: setiap kali ada update status dari pengguna, instance Node.js kita langsung menghantam database secara langsung tanpa perantara penampung yang andal, memicu *thundering herd problem* yang merobohkan seluruh ekosistem backend dalam hitungan menit.

> [!WARNING]
> Jangan pernah membiarkan koneksi WebSocket atau layanan *real-time* berkomunikasi langsung dengan database transaksional utama tanpa lapisan *event broker* atau *caching layer* di tengahnya. Kegagalan memisahkan beban baca-tulis ini adalah bom waktu bagi infrastruktur web modern.

Panik tidak akan menyelamatkan sistem. Sebagai *Principal Systems Architect*, saya harus segera menarik napas dalam-dalam, menyeduh secangkir kopi hitam, dan mulai membuka terminal untuk menelusuri jejak digital yang ditinggalkan oleh bencana ini.

## 2. Jejak Digital: Menelusuri Anomali Log & Lonjakan Metrik

Begitu masuk ke server melalui SSH, langkah pertama saya adalah memeriksa log akses Nginx dan error log dari PM2 yang mengelola proses Node.js. Perintah sederhana di terminal langsung memperlihatkan pemandangan yang mengenaskan:

```bash
# Memantau error log secara real-time dari cluster Node.js
pm2 logs --err --lines 100
```

Log dipenuhi dengan pesan mengerikan: `Error: connect ETIMEDOUT` dan `Too many connections` dari MySQL. Ketika kami meluncurkan fitur *real-time notifications* minggu lalu, kami mengandalkan memori lokal instance Node.js untuk menyimpan array *socket connections*. Masalahnya, ketika trafik melonjak, kami harus menaikkan jumlah *worker* PM2 menggunakan *cluster mode* agar utilisasi CPU 8-core server termanfaatkan secara maksimal. Akibatnya? Instance worker Node.js yang satu tidak tahu menahu tentang koneksi WebSocket yang mendarat di worker sebelah.

Untuk mengatasi masalah sinkronisasi antar-worker ini, sistem sebelumnya mencoba melakukan *polling* ke database MySQL setiap detik demi memeriksa perubahan data. Sebuah keputusan arsitektural naif yang menjadi bumerang ketika ribuan klien terhubung secara bersamaan. Grafik metrik Redis di dashboard kami menunjukkan lonjakan penggunaan memori yang tidak wajar, sementara CPU database terkunci di angka 100% akibat antrean *query* yang menumpuk.

> [!TIP]
> Gunakan alat seperti `htop`, `redis-cli monitor`, dan `pm2 monit` secara bersamaan saat melakukan investigasi insiden *real-time*. Kombinasi ini memberikan visibilitas instan terhadap siapa yang merampok sumber daya CPU dan RAM server Anda.

Dengan pola anomali yang sudah terpetakan dengan jelas dari log dan metrik, akar permasalahan kini sudah berada di depan mata. Saatnya kita bedah secara mendalam apa yang sebenarnya salah dengan desain sistem ini.

## 3. Membongkar Biang Kerok: Akar Masalah yang Sebenarnya

Setelah menarik korelasi antara lonjakan trafik dan kegagalan sistem, kami menemukan dua dosa besar arsitektural pada implementasi awal aplikasi kami. Pertama, penggunaan *in-memory socket store* pada Node.js yang tidak mendukung arsitektur *multi-process cluster*. Ketika Nginx melempar trafik ke instance PM2 yang berbeda, klien A yang terhubung ke Worker 1 tidak akan pernah menerima *broadcast* pesan yang dipicu oleh proses yang ditangani oleh Worker 2.

Kedua, ketiadaan lapisan *Message Broker* yang memisahkan antara produsen data (backend API) dan konsumen data (klien WebSocket). Kami memaksakan database relasional MySQL untuk melayani beban *pub/sub* tingkat tinggi, padahal MySQL dirancang untuk konsistensi transaksional (ACID), bukan untuk antrean pesan berkecepatan tinggi yang bersifat *ephemeral*.

Mari kita lihat perbandingan cepat antara pendekatan lama yang rapuh dan arsitektur baru yang kami bangun:

| Parameter | Pendekatan Lama (Raw DB + In-Memory) | Pendekatan Baru (Node.js + Redis Pub/Sub) |
| :--- | :--- | :--- |
| **Penyimpanan Koneksi** | Array lokal di dalam proses Node.js | Socket.io dikelola dengan *Redis Adapter* |
| **Komunikasi Antar-Worker** | Tidak ada (Terisolasi per core CPU) | Disinkronkan secara instan via Redis Pub/Sub |
| **Beban Database** | *Polling* konstan setiap detik (Tinggi) | Hanya menulis saat ada transaksi mutlak (Rendah) |
| **Skalabilitas** | Gagal total saat *multi-process* | Mudah diskalakan secara horizontal dan vertikal |

> [!IMPORTANT]
> Redis Pub/Sub bekerja dengan pola *fire-and-forget*. Pesan yang dipublikasikan ke sebuah *channel* akan langsung disebarkan ke semua *subscriber* yang mendengarkan saat itu juga tanpa menyimpannya ke disk, menjadikannya sangat cepat dan efisien untuk notifikasi *real-time*.

Mengetahui akar masalah saja tidak cukup; kita membutuhkan tindakan darurat di lapangan untuk menghentikan pendarahan infrastruktur ini dalam hitungan detik.

## 4. Tindakan Darurat: Menghidupkan Kembali Layanan dalam Hitungan Menit

Situasi sudah sangat kritis. Layanan harus segera diselamatkan sebelum manajemen melayangkan teguran keras. Berikut adalah barisan perintah terminal penyelamat darurat (*Emergency Runbook*) yang saya eksekusi langsung di server produksi untuk meredakan beban database dan menstabilkan kembali klaster Node.js dalam waktu kurang dari 60 detik:

```bash
# 1. Hentikan seluruh proses Node.js yang mengalami memory leak dan menghantam database
pm2 delete all

# 2. Flush cache Redis yang bermasalah untuk membebaskan RAM
redis-cli FLUSHALL

# 3. Restart layanan Nginx untuk membersihkan koneksi TCP yang menggantung (TIME_WAIT)
systemctl restart nginx

# 4. Jalankan kembali skrip backend dengan konfigurasi PM2 Cluster yang dibatasi alokasi memori per worker
pm2 start ecosystem.config.js --env production
```

Setelah menjalankan rangkaian perintah di atas, grafik latensi di Grafana mulai turun secara drastis dari rata-rata 4500ms menjadi stabil di bawah 45ms. Koneksi WebSocket kembali normal, dan database MySQL akhirnya bisa bernapas lega karena beban *polling* berhasil dipangkas hingga 95%.

Namun, kemenangan kecil ini tidak boleh membuat terlena. Agar insiden serupa tidak terulang di masa depan, kita perlu merumuskan kebijakan retrospektif yang ketat.

## 5. Kebijakan Retrospektif: Menambal Celah agar Tak Pernah Terulang

Evaluasi pasca-insiden menghasilkan beberapa keputusan arsitektural mutlak yang langsung dimasukkan ke dalam *Engineering Handbook* tim kami. Pertama, implementasi penuh Redis Pub/Sub yang dikombinasikan dengan modul `@socket.io/redis-adapter`. Dengan konfigurasi ini, berapa pun jumlah *worker* PM2 yang kita jalankan di atas server multi-core, pesan dari satu klien akan disebarkan ke seluruh *worker* lain secara mulus melalui Redis.

Berikut adalah contoh implementasi kode arsitektur baru menggunakan Node.js, Socket.io, dan Redis Adapter yang kini berjalan di lini produksi kami:

```javascript
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');

const app = express();
const server = http.createServer(app);

// Konfigurasi Redis Publisher dan Subscriber Client
const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();

async function startServer() {
    await pubClient.connect();
    await subClient.connect();

    const io = new Server(server, {
        cors: { origin: "*" }
    });

    // Menghubungkan Socket.io dengan Redis Adapter untuk multi-process sync
    io.adapter(createAdapter(pubClient, subClient));

    io.on('connection', (socket) => {
        console.log(`Klien terhubung dengan ID: ${socket.id}`);

        socket.on('send_notification', (data) => {
            // Broadcast ke seluruh klien yang terhubung di semua worker PM2
            io.emit('receive_notification', data);
        });

        socket.on('disconnect', () => {
            console.log(`Klien terputus: ${socket.id}`);
        });
    });

    server.loadBalancer = true;
    server.listen(3000, () => {
        console.log('Server real-time berjalan stabil di port 3000');
    });
}

startServer().catch(console.err);
```

Selain perubahan kode, kami juga merevisi file konfigurasi PM2 (`ecosystem.config.js`) agar secara otomatis melakukan *restart* instance apabila penggunaan memori melewati ambang batas tertentu (*max_memory_restart*), mencegah terjadinya *out-of-memory crash* yang tidak terduga:

```javascript
module.exports = {
  apps: [
    {
      name: 'realtime-service',
      script: './server.js',
      instances: 'max',
      exec_mode: 'cluster',
      max_memory_restart: '500M',
      env: {
        NODE_ENV: 'production',
      },
    },
  ],
};
```

> [!TIP]
> Selalu tetapkan parameter `max_memory_restart` pada PM2 ketika menjalankan aplikasi Node.js berperforma tinggi di lingkungan produksi untuk mengantisipasi potensi *memory leak* secara otomatis.

Sistem kini sudah kokoh secara kode dan infrastruktur, langkah terakhir yang wajib dieksekusi adalah memasang sistem peringatan dini agar tim *on-call* tidak lagi terlambat merespons.

## 6. Pasang Alarm: Otomasi Deteksi Dini untuk Tim On-Call

Pengalaman pahit malam itu mengajarkan kita bahwa pemantauan reaktif adalah sebuah bentuk kelalaian. Kini, kami telah mengintegrasikan sistem *alerting* otomatis menggunakan Prometheus dan Alertmanager yang langsung terhubung ke webhook Slack dan PagerDuty tim teknik.

Parameter pemantauan kritis yang kini wajib aktif meliputi:
1. **Redis Memory Usage > 80%**: Mencegah Redis mengalami *Out of Memory (OOM Killer)*.
2. **PM2 Restart Count > 3 dalam 5 menit**: Menandakan adanya *crash loop* pada aplikasi Node.js.
3. **HTTP 5xx Error Rate > 2%**: Deteksi dini kegagalan komunikasi antara Nginx dan backend.

Dengan sistem pengamanan berlapis ini, arsitektur *real-time notification* berbasis Node.js dan Redis Pub/Sub kini tidak hanya berjalan sangat cepat dan efisien, tetapi juga memiliki ketahanan baja yang siap menghadapi lonjakan trafik sebesar apa pun di masa depan. Tidur malam tim *on-call* pun kembali tenang tanpa harus dihantui oleh alarm PagerDuty tengah malam.