# Membangun Real-time Notification System dengan Go, Redis, dan WebSockets

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

---

Pukul 2 pagi. PagerDuty berdering keras membangunkan saya dari tidur lelap. Layar monitor menunjukkan grafik latensi layanan notifikasi melonjak tajam, dan *goroutine* di server Go kami mengalami *spike* mematikan akibat jutaan koneksi WebSocket yang *drop* bersamaan. Di ruang perang digital, tim terbelah menjadi dua kubu: mereka yang ingin mendesain ulang sistem perpesanan *real-time* berbasis arsitektur *Pub/Sub* Redis yang dipadukan dengan manajemen *connection pooling* native Go, melawan faksi yang bersikeras melapisinya secara membabi buta menggunakan Nginx sebagai *reverse proxy* dan *load balancer* di lini depan. Ini bukan sekadar diskusi teoretis di ruang rapat ber-AC, melainkan pertarungan hidup-mati infrastruktur di medan perang produksi yang sebenarnya.

Sebelum kita menyelami detail teknis bagaimana mengelola jutaan koneksi, mari kita lihat matriks perbandingan langsung antara pendekatan arsitektur Go+Redis+WebSockets murni melawan pendekatan konvensional berlapis Nginx di lingkungan produksi berskala masif:

| Metrik Evaluasi | Arsitektur Go + Redis Pub/Sub | Arsitektur Nginx Proxy + Go Backend |
| :--- | :--- | :--- |
| **Kecepatan & Throughput** | 9/10 (Sangat langsung, minim overhead) | 8/10 (Ada hop tambahan di reverse proxy) |
| **Latency (p99)** | 9/10 (< 5ms dalam jaringan lokal) | 7/10 (Tergantung konfigurasi buffer upstream) |
| **Penggunaan RAM** | 8/10 (Sangat efisien, ~10KB per koneksi Go) | 6/10 (Membutuhkan memori ekstra untuk worker Nginx) |
| **Kemudahan Setup** | 7/10 (Butuh pengelolaan state manual di kode) | 9/10 (Konfigurasi Nginx mapan dan mudah) |
| **Ekosistem & Skalabilitas** | 9/10 (Horizontal scaling mulus via Redis Cluster) | 9/10 (Teruji di jutaan web server di seluruh dunia) |

> [!IMPORTANT]
> Memilih arsitektur *real-time* bukan soal teknologi mana yang paling populer di GitHub, melainkan bagaimana model *concurrency* bahasa pemrograman Anda memperlakukan memori fisik ketika klien memutuskan untuk melakukan *disconnect* secara massal.

Mari kita bedah bagaimana kedua pendekatan ini diuji di lapangan, mulai dari ergonomi penulisan kode hingga performa mentahnya di bawah tekanan.

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

Ketika berbicara tentang sistem notifikasi *real-time*, pertarungan filosofis utamanya terletak pada bagaimana kita mendistribusikan beban. Di satu sisi, ada faksi yang percaya bahwa arsitektur monolitik yang diperkuat oleh *event loop* tunggal sudah cukup jika dikonfigurasi dengan benar. Di sisi lain, faksi modern berpendapat bahwa pemisahan tugas secara tegas antara lapisan penerima koneksi (*Go WebSocket server*) dan lapisan distribusi pesan (*Redis Pub/Sub*) adalah satu-satunya jalan menuju skalabilitas horizontal tanpa batas.

Di lapangan, ketika tenggat waktu rilis fitur semakin dekat dan manajer produk menuntut sistem yang mampu mengirimkan jutaan *push notification* detik itu juga, kompleksitas mulai menghantui. Mengandalkan pustaka pihak ketiga tanpa memahami bagaimana *TCP keep-alive* bekerja di balik layar adalah resep bencana. Kita sering kali terjebak dalam ilusi bahwa server kita tangguh, padahal sistem operasi di bawahnya sedang megap-megap kehabisan *file descriptor*.

> [!WARNING]
> Jangan pernah meremehkan batasan sistem operasi `ulimit -n`. Tanpa menaikkan batas jumlah *open files* di level kernel Linux, server Go Anda akan langsung mencatat *error* `too many open files` begitu klien ke-1025 mencoba melakukan *handshake* WebSocket.

Mari kita beralih ke bagaimana pengalaman nyata saat kita mulai menulis baris kode pertama untuk mengikat koneksi-koneksi persisten ini.

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

Menulis server WebSocket di Go terasa seperti mengendarai mobil sport manual: responsif, bertenaga, namun menuntut konsentrasi penuh agar tidak salah oper gigi. Go, dengan model konkurensinya yang elegan melalui *Goroutine* dan *Channel*, membuat penanganan ribuan koneksi bersamaan terasa sangat alami.

Berikut adalah cuplikan implementasi *Hub* manajemen koneksi WebSocket native di Go yang bersih dan siap produksi:

```go
package main

import (
	"log"
	"net/http"
	"github.com/gorilla/websocket"
)

// Client merepresentasikan satu koneksi WebSocket aktif.
type Client struct {
	hub  *Hub
	conn *websocket.Conn
	send chan []byte
}

// Hub memelihara sekumpulan klien aktif dan menyiarkan pesan.
type Hub struct {
	clients    map[*Client]bool
	broadcast  chan []byte
	register   chan *Client
	unregister chan *Client
}

func NewHub() *Hub {
	return &Hub{
		broadcast:  make(chan []byte),
		register:   make(chan *Client),
		unregister: make(chan *Client),
		clients:    make(map[*Client]bool),
	}
}

func (h *Hub) Run() {
	for {
		select {
		case client := <-h.register:
			h.clients[client] = true
		case client := <-h.unregister:
			if _, ok := h.clients[client]; ok {
				delete(h.clients, client)
				close(client.send)
			}
		case message := <-h.broadcast:
			for client := range h.clients {
				select {
				case client.send <- message:
				default:
					close(client.send)
					delete(h.clients, client)
				}
			}
		}
	}
}
```

Sebaliknya, konfigurasi di sisi Nginx untuk memastikan koneksi WebSocket tidak terputus secara prematur akibat *timeout* membutuhkan ketelitian tingkat tinggi. Anda harus secara eksplisit mendefinisikan *header upgrade* agar proxy meneruskan koneksi jangka panjang tersebut dengan mulus:

```nginx
http {
    upstream websocket_backend {
        server 127.0.0.1:8080;
        keepalive 32;
    }

    server {
        listen 80;
        server_name notification.internal;

        location /ws {
            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;
            
            # Timeout krusial untuk koneksi real-time jangka panjang
            proxy_read_timeout 3600s;
            proxy_send_timeout 3600s;
        }
    }
}
```

> [!TIP]
> Selalu aktifkan `keepalive` pada blok `upstream` Nginx. Tanpa direktif ini, Nginx akan membuka koneksi TCP baru ke backend Go untuk setiap request, yang secara drastis akan melambatkan proses *handshake*.

Namun, bagaimana performa gabungan ini ketika ribuan pengguna mulai masuk secara bersamaan? Mari kita buktikan di atas arena uji beban.

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

Untuk menguji ketahanan sistem, kami menggunakan perangkat uji beban modern untuk mensimulasikan 50.000 koneksi konkuren yang aktif mengirimkan *payload* JSON setiap detiknya. Di sinilah letak perbedaan mendasar antara sistem yang dirancang dengan matang dan sistem yang sekadar bisa berjalan di *localhost*.

Ketika Redis Pub/Sub dimasukkan ke dalam persamaan sebagai jembatan komunikasi antar *node* Go, latensi tetap terjaga di bawah 10 milidetik. Redis bertindak sebagai pusat saraf yang mendistribusikan pesan dari satu instans Go ke instans Go lainnya yang terdistribusi di beberapa server berbeda.

```go
import (
	"context"
	"github.com/redis/go-redis/v9"
)

var ctx = context.Background()

func InitRedisPubSub(hub *Hub, redisClient *redis.Client) {
	pubsub := redisClient.Subscribe(ctx, "notification_channel")
	go func() {
		ch := pubsub.Channel()
		for msg := range ch {
			hub.broadcast <- []byte(msg.Payload)
		}
	}()
}
```

> [!IMPORTANT]
> Pastikan Anda menggunakan koneksi *pool* Redis yang terisolasi dan atur kapasitas maksimum koneksi (`PoolSize`) sesuai dengan jumlah core CPU dan kapasitas maksimal *file descriptor* server Anda untuk menghindari *connection starvation*.

Ketika beban dinaikkan hingga menyentuh titik jenuh, server Go murni menunjukkan konsumsi CPU yang sangat linier dan stabil, sementara memori hanya merangkak naik secara perlahan berkat manajemen *garbage collection* Go yang semakin matang di versi-versi terbarunya.

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

Uang berbicara paling keras di ruang server. Ketika infrastruktur Anda membengkak hingga puluhan instans *cloud*, efisiensi RAM dan CPU adalah penentu apakah perusahaan Anda akan merugi atau mendulang laba.

Dalam pengujian kami, satu instans server Go yang menangani 10.000 koneksi WebSocket aktif hanya memakan sekitar 100MB hingga 150MB RAM. Ini adalah angka yang sangat fantastis jika dibandingkan dengan beberapa framework berbasis bahasa lain yang membutuhkan gigabit memori untuk beban kerja serupa. Ketika dipadukan dengan Redis yang di-host di *cluster* terpisah, total biaya infrastruktur dapat ditekan hingga separuhnya.

Namun, penambahan Nginx sebagai lapisan *edge* memberikan keuntungan tambahan berupa proteksi terhadap serangan *Layer 7 DDoS* dan kemampuan *SSL termination* yang terpusat, meski harus dibayar dengan sedikit peningkatan penggunaan RAM pada server *proxy* tersebut.

## 5. Kapan Wajib Menggunakan Arsitektur Go + Redis Pub/Sub Murni

Anda wajib memilih pendekatan langsung menggunakan Go dan Redis Pub/Sub ketika:
- Sistem Anda menuntut latensi ultra-rendah di bawah 5 milidetik untuk interaksi *real-time* tingkat tinggi seperti aplikasi trading atau *live gaming chat*.
- Tim Anda memiliki kapasitas operasional yang memadai untuk mengelola kluster Redis dan memantau kesehatan *node* Go secara mandiri.
- Anda ingin memangkas *hop* jaringan yang tidak perlu guna memaksimalkan efisiensi pemanfaatan sumber daya CPU dan memori di level kernel.

## 6. Kapan Wajib Menggunakan Nginx Load Balancer di Depan Backend Go

Sebaliknya, Anda wajib meletakkan Nginx di lini depan ketika:
- Aplikasi Anda berada di balik arsitektur multi-region yang memerlukan penyeimbangan beban (*load balancing*) tingkat lanjut dan mekanisme *health check* otomatis sebelum lalu lintas mencapai server Go.
- Anda membutuhkan titik pusat untuk menangani sertifikat SSL/TLS, pembatasan laju (*rate limiting*), serta mitigasi serangan siber di lapisan web sebelum menyentuh logika bisnis inti.
- Perusahaan Anda menganut prinsip pemisahan peran infrastruktur yang ketat antara tim *DevOps* yang mengelola *web server* dan tim *Backend* yang menulis kode aplikasi.

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

Pada akhirnya, tidak ada arsitektur yang sempurna untuk segala skenario. Membangun sistem notifikasi *real-time* yang tangguh adalah seni menyeimbangkan antara kesederhanaan kode, performa mentah, dan kemudahan pemeliharaan jangka panjang.

Jika skala Anda masih berada di bawah ratusan ribu pengguna aktif harian, mulailah dengan pendekatan yang bersih: Go sebagai fondasi WebSocket dan Redis sebagai penggerak *Pub/Sub*. Jangan biarkan kompleksitas *proxy* berlapis-lapis memperlambat siklus pengembangan produk Anda. Namun, begitu skala menyentuh jutaan koneksi lintas wilayah, hadirkan Nginx atau *load balancer* enterprise di garis depan untuk menjaga benteng pertahanan Anda tetap kokoh di tengah badai lalu lintas internet.