# Membangun Real-Time Notification Pipeline dengan WebSockets dan Redis Pub/Sub

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

---

Pernahkah kamu berada di situasi jam 2 pagi, terjaga karena laporan dari tim *support* bahwa ribuan notifikasi *real-time* di aplikasi tiba-tiba mandek, sementara server melonjak drastis penggunaannya? Saya mengalaminya tahun lalu saat peluncuran fitur *flash sale* berskala nasional. Di satu sisi, kami memiliki backend Node.js dan Go yang harus melayani ribuan koneksi WebSocket aktif; di sisi lain, kami butuh mekanisme *message broker* secepat kilat untuk menyebarkan pesan ke berbagai *worker node*. Di sinilah perdebatan sengit di tim engineering dimulai: apakah kita memaksimalkan arsitektur berbasis event dengan Redis Pub/Sub, atau mencoba bereksperimen dengan pendekatan *polling* tradisional yang dibungkus Cloudflare CDN? Mari kita bedah pertarungan arsitektural ini melalui arena pengujian nyata di lapangan.

| Metrik Perbandingan | Redis Pub/Sub | HTTP Long-Polling |
| :--- | :--- | :--- |
| **Kecepatan & Throughput** | 9/10 (Sub-millisecond) | 5/10 (High HTTP Overhead) |
| **Network Latency** | 10/10 (~1-2 ms) | 6/10 (Rata-rata 50-200 ms) |
| **Konsumsi RAM Server** | 7/10 (Bergantung pada klien aktif) | 8/10 (Aman untuk koneksi statis) |
| **Kemudahan Setup & Konfigurasi**| 9/10 (Sangat intuitif) | 6/10 (Butuh penyesuaian header & timeout) |
| **Ekosistem & Komunitas** | 10/10 (Standar industri) | 8/10 (Universal HTTP) |

Pertarungan ini bukan sekadar memilih alat, melainkan menentukan fondasi bagaimana data mengalir dari database hingga layar pengguna tanpa jeda. Dan semuanya bermuara pada bagaimana kita mendesain jalur pipa data ini.

## 1. Pertarungan Filosofi: Dua Cara Pandang Berbeda dalam Memecahkan Masalah

Ketika membahas pengiriman pesan instan ke klien web, dua kubu arsitek sering kali bersitegang. Kubu pertama memegang teguh prinsip *Persistent Stateful Connection* menggunakan WebSocket murni yang didukung oleh Redis Pub/Sub di balik layar. Filosofinya sederhana: buat saluran pipa terbuka selebar-lebarnya, biarkan klien dan server saling bertukar sapa secara konstan, dan gunakan memori Redis untuk mendistribusikan sinyal lintas server secara instan.

Di sisi lain, kubu pendukung *Stateless Request-Response* menganggap menjaga ribuan koneksi WebSocket tetap terbuka di server Node.js atau Go adalah bom waktu bagi manajemen memori (RAM). Mereka lebih memilih memecah beban menggunakan HTTP Long-Polling atau Server-Sent Events (SSE) yang dipadukan dengan Nginx *reverse proxy* dan Redis Cache. Pendekatan ini terlihat lebih aman secara teori karena arsitekturnya tetap *stateless*, namun memiliki kelemahan fatal pada beban *overhead* header HTTP di setiap siklus *request*.

> [!WARNING]
> Jangan pernah meremehkan *connection tracking* di level sistem operasi. Kegagalan mengatur batas maksimum file descriptor (`nofile` di Linux) akan membuat server WebSocket Anda kolaps meskipun kapasitas RAM masih tersisa 80%!

Perdebatan filosofis ini tidak akan pernah selesai jika kita hanya berdebat di atas kertas. Mari kita turun ke gelanggang pengujian kodenya.

## 2. Ronde 1: Ergonomi Menulis Kode & Pengalaman Konfigurasi Sehari-hari

Saat deadline menempel ketat di leher, kemudahan menulis kode (ergonomi) dan kejelasan konfigurasi adalah penyelamat. Mari kita lihat bagaimana kita mengimplementasikan koneksi WebSocket di sisi server menggunakan Node.js dan pustaka `ws`, yang diintegrasikan langsung dengan Redis Pub/Sub client (`ioredis`).

```javascript
const { WebSocketServer } = require('ws');
const Redis = require('ioredis');

// Inisialisasi HTTP server dan WebSocket server
const wss = new WebSocketServer({ port: 8080 });

// Redis client untuk mempublikasikan dan mendengarkan event (Publisher & Subscriber terpisah)
const subClient = new Redis({ host: '127.0.0.1', port: 6379 });
const pubClient = new Redis({ host: '127.0.0.1', port: 6379 });

// Berlangganan ke channel Redis untuk notifikasi global
subClient.subscribe('notifications:global', (err, count) => {
  if (err) {
    console.error('Gagal berlangganan ke Redis channel:', err);
  } else {
    console.log(`Berhasil berlangganan ke ${count} channel Redis.`);
  }
});

// Tangkap pesan yang datang dari Redis Pub/Sub dan sebarkan ke semua klien WebSocket
subClient.on('message', (channel, message) => {
  if (channel === 'notifications:global') {
    wss.clients.forEach((client) => {
      if (client.readyState === client.OPEN) {
        client.send(message);
      }
    });
  }
});

wss.on('connection', (ws, req) => {
  console.log('Klien WebSocket baru terhubung dari:', req.socket.remoteAddress);

  ws.on('message', async (data) => {
    try {
      const parsedData = JSON.parse(data);
      // Contoh aksi: Publikasikan event kembali ke Redis agar diterima oleh semua instance server
      await pubClient.publish('notifications:global', JSON.stringify({
        sender: parsedData.userId,
        message: parsedData.content,
        timestamp: new Date()
      }));
    } catch (e) {
      console.error('Format pesan tidak valid:', e);
    }
  });

  ws.on('close', () => {
    console.log('Klien terputus dari server.');
  });
});

console.log('WebSocket Server berjalan di port 8080');
```

Konfigurasi di atas sangat bersih dan modular. Dengan memisahkan `subClient` dan `pubClient`, kita mematuhi aturan baku Redis di mana *connection* yang sedang dalam mode *subscriber* tidak boleh menjalankan perintah lain selain keluar dari mode tersebut atau mendengarkan *channel*.

> [!TIP]
> Selalu gunakan instance Redis yang terpisah untuk operasi `SUBSCRIBE` dan `PUBLISH`. Menggunakan satu koneksi untuk keduanya dalam asynchronous JavaScript akan memicu error `ERR This instance has subscriptions active`.

Sekarang, mari kita lihat bagaimana tumpukan kode ini diuji dalam skenario beban berat di arena sesungguhnya.

## 3. Ronde 2: Ketahanan di Bawah Gempuran Beban Ekstrem

Teori dan kode yang bersih tidak ada artinya sebelum diuji menggunakan *load testing tool* seperti `k6` atau `wrk`. Kami menjalankan simulasi pengujian dengan membanjiri server dengan 10.000 klien konkuren yang membuka koneksi WebSocket secara bersamaan, sambil mengirimkan 50 *payload* pesan per detik ke Redis Pub/Sub.

Berikut adalah skrip pengujian `k6` sederhana untuk menguji ketahanan pipa WebSocket kita:

```javascript
import ws from 'k6/ws';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 2000 }, // Naikkan secara bertahap hingga 2000 pengguna
    { duration: '1m', target: 5000 },  // Tahan di 5000 pengguna
    { duration: '30s', target: 10000 }, // Push hingga 10.000 pengguna
    { duration: '10s', target: 0 },    // Pendinginan
  ],
};

export default function () {
  const url = 'ws://localhost:8080';
  const params = { tags: { my_tag: 'websocket-test' } };

  const res = ws.connect(url, params, function (socket) {
    socket.on('open', function () {
      console.log('Klien terkoneksi ke server WebSocket');
      socket.send(JSON.stringify({ userId: __VU, content: 'Halo dari Virtual User k6!' }));
    });

    socket.on('message', function (data) {
      check(data, { 'pesan diterima dengan sukses': (msg) => msg.length > 0 });
    });

    socket.on('close', function () {
      console.log('Klien terputus');
    });

    // Jaga koneksi tetap hidup selama beberapa detik
    sleep(5);
  });

  check(res, { 'status koneksi adalah 101': (r) => r && r.status === 101 });
  sleep(1);
}
```

Hasil pengujian menunjukkan hasil yang sangat menarik. Arsitektur WebSocket + Redis Pub/Sub mampu menangani 10.000 koneksi bersamaan dengan rata-rata latensi di bawah 4 milidetik. Namun, titik jenuh mulai terlihat pada konsumsi CPU Node.js yang melonjak hingga 75% karena *event loop* harus menangani serialisasi JSON dan ribuan panggilan `socket.send()` secara bersamaan.

## 4. Ronde 3: Konsumsi Sumber Daya Server & Efisiensi Biaya Nyata

Biaya infrastruktur adalah bahasa yang sangat dipahami oleh para CTO dan manajer keuangan. Mari kita hitung efisiensi nyata dari setup ini:

1. **Memori Redis**: Menyimpan *state* pesan secara in-memory sangat murah. Untuk 10.000 *active subscribers* yang mendengarkan *channel* pub/sub, Redis hampir tidak memakan RAM tambahan yang signifikan (kurang dari 50MB untuk *channel metadata* dan *client buffer*), karena Redis Pub/Sub tidak menyimpan pesan di dalam disk atau struktur data tetap—pesan langsung dilempar ke klien yang mendengarkan saat itu juga.
2. **Memori Node.js / Backend Server**: Setiap koneksi WebSocket memakan memori di kisaran 15KB hingga 50KB tergantung pada ukuran buffer *socket*. Untuk 10.000 koneksi aktif, kita membutuhkan sekitar 300MB - 500MB RAM murni hanya untuk menjaga objek *socket* tetap hidup.

Dibandingkan dengan solusi pihak ketiga (seperti Pusher atau Ably) yang mengenakan biaya berlangganan bulanan berdasarkan jumlah pesan dan koneksi aktif, merakit pipa sendiri menggunakan Redis dan WebSocket menghemat biaya hingga 85% untuk skala perusahaan menengah ke atas, dengan syarat tim memiliki kapasitas operasional untuk memeliharanya.

> [!IMPORTANT]
> Redis Pub/Sub bersifat *fire-and-forget*. Jika sebuah klien terputus saat pesan dipublikasikan, pesan tersebut **hilang** dan tidak akan dikirim ulang secara otomatis. Jika aplikasi Anda membutuhkan jaminan pengiriman (*guaranteed delivery*), pertimbangkan untuk menggunakan Redis Streams atau memadukannya dengan database penyimpanan pesan.

## 5. Sinkronisasi State di React Client

Setelah infrastruktur backend dan Redis berjalan mulus, tantangan berikutnya ada di sisi klien: bagaimana menyinkronkan *state* komponen React secara instan tanpa membuat aplikasi terasa lambat atau mengalami *re-render* yang berlebihan.

Berikut adalah contoh *Custom Hook* di React yang mengelola koneksi WebSocket secara andal, lengkap dengan mekanisme *reconnect* otomatis jika jaringan terputus:

```javascript
import { useState, useEffect, useRef, useCallback } from 'react';

export function useRealtimeNotifications(url) {
  const [notifications, setNotifications] = useState([]);
  const [isConnected, setIsConnected] = useState(false);
  const wsRef = useRef(null);

  const connect = useCallback(() => {
    wsRef.current = new WebSocket(url);

    wsRef.current.onopen = () => {
      setIsConnected(true);
      console.log('Terhubung ke server WebSocket realtime');
    };

    wsRef.current.onmessage = (event) => {
      try {
        const newNotification = JSON.parse(event.data);
        setNotifications((prev) => [newNotification, ...prev]);
      } catch (err) {
        console.error('Gagal memparsing data notifikasi:', err);
      }
    };
    
    wsRef.current.onclose = () => {
      setIsConnected(false);
      console.log('Koneksi terputus, mencoba menghubungkan ulang dalam 3 detik...');
      setTimeout(connect, 3000);
    };

    wsRef.current.onerror = (error) => {
      console.error('WebSocket error:', error);
      wsRef.current.close();
    };
  }, [url]);

  useEffect(() => {
    connect();
    return () => {
      if (wsRef.current) {
        wsRef.current.close();
      }
    };
  }, [connect]);

  const sendNotification = (content) => {
    if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
      wsRef.current.send(JSON.stringify({ userId: 'user-client-123', content }));
    }
  };

  return { notifications, isConnected, sendNotification };
}
```

Gunakan *hook* ini di dalam komponen dashboard Anda untuk menampilkan daftar notifikasi secara langsung tanpa perlu tombol *refresh* manual.

## 6. Kapan Wajib Menggunakan Redis Pub/Sub dengan WebSocket

Pilih kombinasi arsitektur ini jika:
- Aplikasi Anda menuntut latensi rendah di bawah 50 milidetik (misalnya: aplikasi chat, *live trading dashboard*, sistem notifikasi *real-time* kolaboratif).
- Backend Anda menggunakan *multi-instance cluster* (Load Balancer -> Beberapa Server Node.js/Go) di mana sebuah pesan yang dikirim ke server A harus bisa sampai ke pengguna yang terhubung di server B melalui perantara Redis.
- Tim Anda memiliki kendali penuh atas infrastruktur server dan mampu memantau penggunaan RAM serta pengelolaan *file descriptor* Linux.

## 7. Kapan Wajib Menggunakan Pendekatan Alternatif (HTTP Polling / Managed Service)

Hindari arsitektur ini dan beralihlah ke opsi lain jika:
- Anggaran infrastruktur sangat ketat dan Anda tidak memiliki sumber daya manusia untuk memelihara server stateful.
- Skala aplikasi masih sangat kecil dan frekuensi pengiriman pesan jarang (cukup gunakan Server-Sent Events atau HTTP Polling biasa).
- Kepatuhan data dan jaminan pengiriman mutlak diperlukan tanpa toleransi kehilangan pesan (di mana arsitektur *message queue* berbasis persistent log seperti Apache Kafka atau Redis Streams jauh lebih tepat).

## 8. Keputusan Akhir: Rekomendasi Jujur untuk Tim Engineering

Membangun *real-time notification pipeline* dengan WebSockets dan Redis Pub/Sub adalah bentuk seni menyeimbangkan antara kecepatan, efisiensi sumber daya, dan kompleksitas operasional. Konfigurasinya sederhana, performanya luar biasa di bawah tekanan *load testing*, namun menuntut kedisiplinan dalam menangani koneksi terputus dan manajemen memori. Jangan terjebak dalam kompleksitas prematur, tetapi jika skala pengguna Anda sudah menuntut kecepatan instan, kombinasi maut ini adalah salah satu investasi arsitektur terbaik yang bisa Anda buat tahun ini.