# Membangun Real-time Notification System dengan Node.js dan WebSocket

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

---

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

Pukul 02.14 WIB. PagerDuty di sebelah bantal bergetar hebat dengan nada paling agresif yang pernah dirancang manusia. Layar ponsel langsung menyala membabi buta, memuntahkan notifikasi dari sistem pemantauan: *"CRITICAL: High CPU Usage & WebSocket Gateway Unresponsive on Node.js Cluster"*. Nyawa tidur malam saya langsung melayang seketika. Di dashboard Grafana, grafik metrik koneksi aktif yang biasanya melandai indah mendadak patah ke atas, menembus angka 250.000 concurrent connections, sementara grafik *latency* melompat dari kisaran 45 milidetik langsung menyentuh 4.200 milidetik sebelum akhirnya grafik *HTTP 502 Bad Gateway* dari Nginx meroket tajam.

Sistem real-time notification kami yang dibangun di atas Node.js tiba-tiba lumpuh total. Para pengguna setia mulai protes di media sosial karena notifikasi transaksi finansial mereka tertunda bermata-mata. Sebagai Principal Systems Architect, saya tahu persis bahwa kepanikan tidak akan menyelesaikan masalah. Segera saya buka laptop, menyeduh kopi hitam pekat, dan mulai menyelam ke dalam lambung server produksi untuk mencari tahu apa yang sebenarnya membunuh infrastruktur kami di tengah malam buta ini.

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

Langkah pertama seorang detektif infrastruktur bukanlah langsung mengetik perintah sembarangan, melainkan mengumpulkan barang bukti. Saya langsung melompat ke terminal dan mengakses mesin cluster menggunakan SSH, lalu memantau log error pada reverse proxy Nginx dan proses Node.js yang berjalan di balik layar.

```bash
# Memeriksa log error Nginx untuk melihat pola kegagalan koneksi upstream
tail -n 100 /var/log/nginx/error.log | grep -E "upstream timed out|connection refused"

# Memeriksa penggunaan memori proses Node.js secara real-time
pm2 monit
```

Hasilnya langsung menampar kesadaran saya. Nginx mengeluhkan *"upstream timed out (110: Connection timed out) while reading response header from upstream"*, sementara panel `pm2 monit` menunjukkan bahwa heap memory dari setiap instance Node.js merangkak naik secara konstan tanpa pernah turun, menyentuh angka 1.4 GB dari batas maksimum V8 engine sebesar 1.5 GB. Garbage Collector (GC) terlihat bekerja kerja rodi hingga menghabiskan 90% kapasitas CPU hanya untuk melakukan *Full GC*, namun memori tetap saja tidak mau dilepaskan. Ada sesuatu yang menahan referensi objek di dalam memori, dan itu bukan pertanda baik untuk arsitektur aplikasi web modern yang mengandalkan koneksi persisten.

> [!WARNING]
> Jangan pernah mengabaikan peringatan peningkatan heap memory pada aplikasi Node.js yang menangani *long-lived connections*. Kenaikan memori yang linier tanpa stabilisasi adalah indikator klasik dari *Memory Leak* yang mematikan.

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

Setelah membongkar kode sumber aplikasi, saya menemukan kombinasi maut yang memicu bencana malam ini. Pertama, keputusan awal dalam memilih protokol komunikasi ternyata kurang matang. Tim kami menggunakan Long Polling pada layanan *legacy* sementara klien baru memaksa menggunakan WebSocket murni tanpa mekanisme *heartbeat* atau *ping/pong* yang disiplin. Ketika jaringan seluler ribuan pengguna mengalami *drop* sesaat, klien terputus secara tidak wajar (*abrupt disconnection*), meninggalkan *socket reference* yang menggantung di dalam RAM server.

Lebih parah lagi, arsitektur *clustering* kami menggunakan pendekatan *in-memory event emitter* tanpa menggunakan Redis Pub/Sub Adapter. Ketika trafik diskalakan secara horizontal di belakang Nginx load balancer menggunakan algoritma *IP Hash* yang pincang, pesan broadcast antar server menjadi kacau. Objek *socket instance* yang terputus gagal dibersihkan dari *global array* karena event `close` tertutup oleh exception yang tidak tertangkap (*unhandled rejection*).

Mari kita bedah perbandingan arsitektur yang kami evaluasi saat merombak ulang sistem ini:

*   **WebSocket:** Ideal untuk komunikasi dua arah *real-time* berlatensi rendah, namun membutuhkan manajemen memori yang sangat ketat dan *stateful server architecture*.
*   **Server-Sent Events (SSE):** Sangat bagus untuk aliran data satu arah dari server ke klien melalui HTTP murni, mudah melewati firewall korporat, namun kurang efisien jika klien harus sering mengirim data balik ke server.
*   **Long Polling:** Solusi paling boros sumber daya karena membuka dan menutup koneksi HTTP berulang kali, menyebabkan lonjakan beban pada Nginx dan *connection pool* database.

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

Klien tidak bisa menunggu analisis mendalam selesai. Layanan harus segera hidup sekarang juga. Saya mengeksekusi serangkaian perintah darurat untuk membersihkan *stale connections*, melakukan *rolling restart* pada cluster Node.js, dan mengaktifkan pembatasan koneksi di level Nginx untuk menyelamatkan server dari serangan *thundering herd problem*.

```bash
# Melakukan graceful reload Nginx setelah memperbarui konfigurasi timeout
sudo nginx -t && sudo nginx -s reload

# Melakukan restart instance Node.js secara bertahap tanpa downtime menggunakan PM2
pm2 reload all --update-env

# Membuang koneksi TCP yang menggantung menggunakan ss (Socket Statistics)
sudo ss -K '( sport = :443 )' state fin-wait-1
```

Selain itu, saya segera menyuntikkan konfigurasi Nginx yang dioptimalkan untuk menahan terjangan koneksi WebSocket dan mencegah *timeout* prematur:

```nginx
http {
    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }

    server {
        listen 443 ssl;
        server_name notification.production.internal;

        location /ws/ {
            proxy_pass http://node_websocket_cluster;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
            proxy_set_header Host $host;
            
            # Konfigurasi timeout krusial untuk mencegah socket terputus sepihak
            proxy_read_timeout 3600s;
            proxy_send_timeout 3600s;
            proxy_buffering off;
        }
    }
}
```

Langkah taktis ini berhasil meredam lonjakan beban. Grafik latensi kembali landai ke angka 30 milidetik, dan CPU usage turun drastis ke bawah 25%. Namun, ini baru pemadam kebakaran; akar masalahnya harus kita kubur selamanya.

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

Pengalaman pahit ini memaksa kami merombak total fondasi sistem notifikasi. Kami meninggalkan pendekatan *in-memory storage* yang naif dan beralih sepenuhnya ke implementasi Redis Pub/Sub Adapter menggunakan library `socket.io-redis` (atau `=@socket.io/redis-adapter`). Dengan cara ini, setiap pesan dapat disiarkan secara sinkron ke seluruh node di dalam cluster tanpa membebani memori lokal aplikasi.

Berikut adalah cuplikan kode implementasi Node.js modern yang aman dari memori bocor dan dilengkapi dengan mekanisme *heartbeat* otomatis:

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

const httpServer = createServer();
const io = new Server(httpServer, {
  pingInterval: 25000, // Deteksi koneksi mati setiap 25 detik
  pingTimeout: 20000,  // Batas waktu tunggu respon pong
});

// Konfigurasi Redis Pub/Sub untuk Scaling Horizontal
const pubClient = createClient({ url: 'redis://redis-cluster.internal:6379' });
const subClient = pubClient.duplicate();

Promise.all([pubClient.connect(), subClient.connect()]).then(() => {
  io.adapter(createAdapter(pubClient, subClient));
  console.log('Redis Adapter successfully connected for WebSocket scaling.');
});

io.on('connection', (socket) => {
  console.log(`Client connected: ${socket.id}`);

  // Tangani event kustom dengan aman dan bungkus dalam try-catch
  socket.on('subscribe_channel', async (channelId) => {
    try {
      await socket.join(channelId);
      socket.emit('subscribed', { status: 'success', channel: channelId });
    } catch (err) {
      console.error(`Failed to join channel: ${err.message}`);
    }
  });

  // Pembersihan memori otomatis saat klien terputus
  socket.on('disconnect', (reason) => {
    console.log(`Client disconnected: ${socket.id}, reason: ${reason}`);
    // Hapus referensi manual jika ada objek kustom yang disimpan di socket scope
    socket.removeAllListeners();
  });
});

httpServer.listen(3000, () => {
  console.log('WebSocket Gateway running smoothly on port 3000');
});
```

> [!TIP]
> Selalu tetapkan nilai `pingInterval` dan `pingTimeout` pada WebSocket server Anda. Koneksi TCP yang terputus di tengah jalan tanpa paket FIN (karena kabel dicabut atau kehilangan sinyal seluler) tidak akan terdeteksi oleh OS secara instan tanpa adanya *application-layer heartbeat*.

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

Agar insiden pukul dua pagi ini tidak terulang tanpa peringatan dini, kami memperketat sistem observabilitas. Prometheus dan Grafana kini dikonfigurasi untuk memicu *alert* tingkat *Warning* jauh sebelum sistem mencapai titik jenuh. Berikut adalah aturan Prometheus Alerting Rule yang kami terapkan untuk memantau kesehatan WebSocket gateway:

```yaml
groups:
  - name: websocket_alerts
    rules:
      - alert: HighMemoryUsageNodeJS
        expr: nodejs_heap_size_used_bytes / nodejs_heap_size_total_bytes > 0.85
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Node.js heap memory usage is above 85%"
          description: "Instance {{ $labels.instance }} is experiencing high memory consumption, potential memory leak in WebSocket connections."

      - alert: WebSocketConnectionSpike
        expr: rate(websocket_connections_total[1m]) > 5000
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Sudden surge in incoming WebSocket connections"
          description: "Inbound connection rate has exceeded safety thresholds, inspect load balancer and rate limiters."

      - alert: RedisPubSubDisconnected
        expr: redis_connected_clients == 0
        for: 1m
        labels:
          severity: page
        annotations:
          summary: "Redis adapter connection lost"
          description: "Cluster nodes have lost synchronization via Redis Pub/Sub."

```

Dengan sistem deteksi dini ini, tim *on-call* tidak lagi bertindak sebagai pemadam kebakaran yang datang terlambat, melainkan sebagai penjaga gerbang yang mengantisipasi badai sebelum ia merusak benteng pertahanan. Arsitektur real-time kami kini berdiri kokoh, siap melayani ratusan ribu pengguna dengan stabil, aman, dan tanpa kejutan di tengah malam.