L
React ๐ŸฅŠ Head-to-Head Architectural Battle โ€ข Literally Web ID

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

โฑ๏ธ 15 menit baca โ€ข ๐Ÿ“… 21 Oct 2026 โ€ข ๐Ÿ‘๏ธ 1 views
Membangun Real-Time Notification Pipeline dengan WebSockets dan Redis Pub/Sub
๐Ÿ”Š Dengarkan Artikel Ini

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).

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:

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:

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.

๐Ÿท๏ธ Tags: #Nginx #High Concurrency #Performance #Sysadmin #Web Server

Apakah panduan tutorial ini membantu Anda?

Masukan Anda sangat berharga untuk terus memperbarui dan menyempurnakan kualitas materi teknis kami.

๐ŸŽ‰

Terima kasih banyak atas apresiasi Anda!

Tim kami terus menyempurnakan tutorial agar selalu akurat dengan versi teknologi terbaru.

Bantu Kami Menyempurnakan Materi Ini

๐Ÿ—บ๏ธ Jalur Belajar #React Rangkaian Panduan
← Materi Sebelumnya

Membangun Real-Time Collaborative Editor dengan React dan WebSocket

Akhir Seri → Anda telah mencapai materi terbaru di seri ini.
โšก Literally Tech Digest

Update Tutorial & Developer Tools Terbaru Langsung ke Email Anda

Dapatkan kurasi artikel arsitektur software, tips DevOps, panduan database, serta rilis fitur developer utility gratis setiap minggu. Bebas spam, bisa berhenti kapan saja.

โณ Mencari artikel...
Tutorial & Artikel Terkait
Developer Tools
Tidak ditemukan tutorial atau tool dengan kata kunci "".
Ketik minimal 2 karakter untuk mencari artikel atau nama tools.
Tekan ESC untuk menutup Cari di Semua Artikel →