# Arsitektur Real-time Scalable dengan Go, WebSockets, dan Redis Pub/Sub

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

---

Pukul 2 pagi di ruang server, alarm PagerDuty berbunyi melengking. Dashboard Grafana kami menyala merah menyala terang: 500.000 koneksi WebSocket aktif mendadak terputus serentak, dan Node.js cluster yang menangani gateway kami mengalami out-of-memory (OOM) killer. Di sinilah perdebatan sengit di ruang arsitek dimulai: apakah kita tetap mempertahankan ekosistem event-loop tunggal dengan segala kenyamanannya, atau melakukan migrasi radikal ke model konkuren murni menggunakan Go, dipadukan dengan Redis Pub/Sub untuk mendistribusikan beban lintas node? 

Mari kita bedah langsung performa kedua pendekatan ini melalui matriks perbandingan head-to-head di lapangan.

| Metrik Perbandingan | Node.js + Cluster Mode | Go + Goroutines + Redis |
| :--- | :--- | :--- |
| **Kecepatan / Throughput** | 6/10 (Terbatasi Event Loop) | **9.5/10 (Konkurensi Murni)** |
| **Latency (p99)** | 7/10 (GC Pause Spike) | **9/10 (Prediktabilitas Tinggi)** |
| **Konsumsi RAM** | 5/10 (~2MB - 4MB per koneksi) | **9/10 (~2KB per goroutine)** |
| **Setup & Kompleksitas** | **8/10 (Sangat Cepat)** | 6/10 (Kurva Belajar Curam) |
| **Ekosistem Real-time** | **9/10 (Socket.io matang)** | 8/10 (Gorilla/Gobwas cepat) |

> [!WARNING]
> Jangan pernah meremehkan overhead memori pada skala ratusan ribu koneksi. Selisih beberapa megabita per socket berarti perbedaan antara server berjalan stabil atau AWS bill yang membengkak luar biasa.

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

Saat mendesain sistem real-time skala besar, kita berhadapan dengan dua mazhab arsitektur yang saling bertolak belakang. Di satu sisi, kita punya pendekatan event-driven berbasis Single-Threaded Event Loop yang dipopulerkan oleh Node.js. Di sisi lain, kita memiliki mazhab CSP (Communicating Sequential Processes) yang diusung oleh Go melalui Goroutines dan Channels.

Bayangkan Anda sedang mengelola sebuah stasiun kereta api. Mazhab pertama menugaskan satu petugas super cepat untuk melayani semua loket penjualan tiket secara bergantian dengan sangat cekatan. Ini bekerja sangat luar biasa hingga ada satu penumpang yang membawa tumpukan dokumen bermasalah dan menghambat antrean seluruh stasiun. Itulah yang terjadi saat Garbage Collection (GC) atau operasi blocking CPU terjadi pada event-loop tunggal.

Sementara itu, mazhab Go menugaskan ribuan petugas kasir mandiri yang masing-masing bekerja secara independen dengan konsumsi energi yang sangat minim. Jika satu loket mengalami kendala, loket lainnya tetap melayani jutaan penumpang tanpa terpengaruh sedikit pun. Di sinilah kekuatan Go Concurrency Model & Goroutines bersinar terang dalam menangani jutaan koneksi simultan.

Transisi dari filosofi teoretis ini membawa kita langsung pada kenyataan pahit saat jari-jari kita mulai mengetikkan baris kode pertama di editor teks.

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

Menulis kode WebSocket di Node.js terasa seperti menikmati secangkir kopi hangat di sore hari: cepat, intuitif, dan pustaka seperti `socket.io` merangkum semua kerumitan protokol dalam beberapa baris kode saja. Namun, ketika skala trafik melonjak dan Anda harus melakukan sinkronisasi state antar-node menggunakan Redis adapter, kompleksitas tersembunyi mulai merayap naik.

Di kubu seberang, Go menuntut disiplin tingkat tinggi. Anda tidak disuguhi fungsi pembungkus instan yang ajaib. Anda harus menangani upgrade koneksi HTTP ke WebSocket secara manual, mengelola buffer pembacaan, hingga merancang mekanisme ping-pong frame sendiri. Namun, begitu fondasi tersebut terbangun, Anda mendapatkan kendali penuh atas setiap byte data yang mengalir.

Mari kita lihat cuplikan implementasi dasar pengelolaan koneksi WebSocket berkinerja tinggi menggunakan bahasa Go:

```go
package main

import (
	"log"
	"net/http"
	"time"

	"github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	CheckOrigin: func(r *http.Request) bool {
		return true // Sesuaikan dengan kebijakan CORS production Anda
	},
}

func handleConnections(w http.ResponseWriter, r *http.Request) {
	ws, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Printf("Gagal melakukan upgrade koneksi: %v", err)
		return
	}
	defer ws.Close()

	// Setiap klien mendapatkan goroutine independen
	go handleClientMessages(ws)
}

func handleClientMessages(ws *websocket.Conn) {
	for {
		_, msg, err := ws.ReadMessage()
		if err != nil {
			log.Printf("Koneksi terputus: %v", err)
			break
		}
		log.Printf("Pesan diterima: %s", string(msg))
	}
}

func main() {
	http.HandleFunc("/ws", handleConnections)
	log.Println("WebSocket server berjalan di port 8080...")
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal("Server error: ", err)
	}
}
```

> [!TIP]
> Selalu atur ukuran `ReadBufferSize` dan `WriteBufferSize` sesuai dengan payload rata-rata aplikasi Anda untuk menghindari alokasi memori heap yang tidak perlu pada Garbage Collector Go.

Kode di atas baru menangani satu instance server. Tantangan sebenarnya muncul ketika server Anda di-scale out menjadi beberapa pod di Kubernetes, memaksa kita masuk ke medan pertempuran pengujian beban yang sesungguhnya.

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

Pengujian performa menggunakan `k6` atau `wrk` adalah momen kebenaran. Kami mensimulasikan 100.000 klien konkuren yang mengirimkan pesan setiap detik ke kluster gateway. 

Pada pengujian menggunakan pendekatan Node.js, grafik latensi p99 melonjak drastis hingga mencapai 1200ms begitu Garbage Collector membersihkan memori heap yang membengkak akibat objek JSON yang masif. CPU utilization terpantau pincang karena beban terpusat pada thread utama.

Sebaliknya, ketika kami menjalankan implementasi Go yang terintegrasi dengan Redis Pub/Sub Scaling Strategy, situasinya berubah drastis. Karena setiap koneksi ditangani oleh goroutine berukuran 2KB, penggunaan RAM tetap stabil di angka yang sangat rendah. Redis Pub/Sub bertindak sebagai saraf pusat yang mendistribusikan pesan antar-node kluster secara instan tanpa hambatan.

Berikut adalah konfigurasi klien Redis Pub/Sub dalam Go untuk memastikan pesan disiarkan ke seluruh kluster server WebSocket secara seamless:

```go
package main

import (
	"context"
	"fmt"
	"github.com/go-redis/redis/v8"
)

var ctx = context.Background()

func SetupRedisPubSub() *redis.Client {
	rdb := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "", // Gunakan password di production!
		DB:       0,
	})

	// Verifikasi koneksi
	if err := rdb.Ping(ctx).Err(); err != nil {
		panic(fmt.Sprintf("Gagal terhubung ke Redis: %v", err))
	}

	return rdb
}

func PublishMessage(rdb *redis.Client, channel string, message string) {
	err := rdb.Publish(ctx, channel, message).Err()
	if err != nil {
		fmt.Printf("Gagal mempublikasikan pesan ke Redis: %v\n", err)
	}
}

func SubscribeToChannel(rdb *redis.Client, channel string) {
	subscriber := rdb.Subscribe(ctx, channel)
	defer subscriber.Close()

	ch := subscriber.Channel()
	for msg := range ch {
		// Broadcast pesan yang diterima dari Redis ke seluruh WebSocket klien lokal
		fmt.Printf("Pesan diterima dari Redis channel %s: %s\n", msg.Channel, msg.Payload)
	}
}
```

Namun, menjaga ribuan koneksi tetap hidup di balik load balancer dan firewall membutuhkan trik khusus yang sering dilupakan para engineer pemula.

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

Menjaga koneksi tetap terbuka dalam jangka panjang rentan terhadap pemutusan sepihak oleh NAT gateway, Nginx reverse proxy, atau Cloudflare timeout. Di sinilah WebSocket Keep-Alive & Heartbeat Tuning menjadi penyelamat infrastruktur Anda.

Tanpa mekanisme ping-pong berkala, koneksi idle akan dianggap mati oleh jaringan perantara, meninggalkan socket zombie yang memakan sumber daya RAM server secara sia-sia. Di Go, kita dapat mengimplementasikan pembaca dan penulis deadline secara presisi menggunakan fungsi bawaan paket `net`.

Berikut snippet konfigurasi heartbeat yang robust untuk mencegah koneksi zombie:

```go
package main

import (
	"time"
	"github.com/gorilla/websocket"
)

const (
	// Waktu tunggu maksimal sebelum write dianggap timeout
	writeWait = 10 * time.Second

	// Interval pengiriman ping ke klien
	pingPeriod = (pongWait * 9) / 10

	// Waktu tunggu maksimal untuk menerima pong dari klien
	pongWait = 60 * time.Second
)

func configureHeartbeat(ws *websocket.Conn) {
	ws.SetReadDeadline(time.Now().Add(pongWait))
	ws.SetPongHandler(func(string) error {
		ws.SetReadDeadline(time.Now().Add(pongWait))
		return nil
	})

	ticker := time.NewTicker(pingPeriod)
	go func() {
		defer ticker.Stop()
		for range ticker.C {
			ws.SetWriteDeadline(time.Now().Add(writeWait))
		if err := ws.WriteMessage(websocket.PingMessage, nil); err != nil {
				return
			}
		}
	}()
}
```

> [!IMPORTANT]
> Selalu pastikan interval `pingPeriod` lebih pendek daripada `pongWait` dan timeout firewall load balancer Anda (biasanya diset 60 detik pada AWS ALB).

Efisiensi dari pengelolaan memori goroutine dan heartbeat yang ketat ini langsung berdampak pada tagihan cloud bulanan yang jauh lebih ramping. Sekarang, mari kita tentukan kapan Anda harus memilih jalur ekstrem ini.

## 5. Kapan Wajib Menggunakan Go dengan Goroutines dan Redis Pub/Sub

Anda wajib mengambil jalur arsitektur Go dan Redis ketika aplikasi Anda memasuki ranah enterprise skala masif:
- Proyek Anda diproyeksikan melayani lebih dari 50.000 hingga jutaan koneksi WebSocket simultan.
- Latency konsisten di bawah 10ms adalah harga mati (misalnya platform trading finansial, game multiplayer real-time, atau sistem live-bidding).
- Anda ingin menekan biaya infrastruktur cloud secara drastis dengan meminimalisir jumlah instance server yang dibutuhkan.
- Tim Anda memiliki kapasitas untuk menulis kode sistem tingkat rendah dan mengelola manajemen konkurensi secara mandiri.

Namun, tidak semua proyek membutuhkan senjata seberat ini. Ada kalanya kesederhanaan adalah prioritas utama.

## 6. Kapan Wajib Menggunakan Stack Konvensional (Seperti Node.js / Socket.io)

Pilihlah ekosistem event-loop konvensional apabila:
- Skala aplikasi Anda masih berada di bawah 10.000 koneksi aktif harian.
- Tim Anda didominasi oleh Frontend Engineer yang ingin membangun fitur real-time dengan cepat tanpa harus memahami manajemen pointer dan goroutine.
- Fitur real-time hanyalah pelengkap kecil (misalnya notifikasi toast sederhana), bukan inti dari produk utama.
- Kecepatan rilis (time-to-market) jauh lebih krusial dibandingkan optimasi performa tingkat milidetik.

Memilih teknologi yang salah untuk masalah yang salah hanya akan mendatangkan bencana teknis di kemudian hari.

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

Arsitektur real-time bukanlah ajang pamer teknologi mana yang paling rumit, melainkan tentang ketepatan memilih alat untuk menyelesaikan masalah bisnis. Go, WebSockets, dan Redis Pub/Sub adalah kombinasi mematikan yang telah teruji menopang sistem skala raksasa dengan efisiensi sumber daya yang luar biasa.

Jangan tergiur bermigrasi ke Go jika aplikasi Anda masih berjalan santai di bawah 5000 pengguna aktif. Namun, begitu lonjakan trafik korporat menghantam pintu gerbang sistem Anda, bersiaplah untuk meninggalkan zona nyaman event-loop tunggal, rangkul model konkurensi Go, dan saksikan sistem Anda berdiri kokoh di tengah badai beban ekstrem.