# Arsitektur Real-Time Web: Membangun Web Server WebSocket Berkinerja Tinggi Menggunakan Golang dan Redis Pub/Sub

> Diterbitkan oleh Literally Web ID pada 21 Oct 2026
> Sumber: https://literally.web.id/articles/arsitektur-real-time-web-membangun-web-server-websocket-berkinerja-tinggi-menggunakan-golang-dan-redis-pubsub

---

## 1. Titik Awal Masalah: Mengapa Arsitektur Lama Sering Ambruk di Produksi

Ingat malam jumat tiga tahun lalu ketika *flash sale* produk kolaborasi artis mendadak meluncur, dan sistem chat real-time di platform kami langsung *melt down*? CPU server melompat ke angka 100%, memori membengkak hingga RAM 32GB habis tersedot, dan ribuan klien terputus secara bersamaan. Masalahnya bukan karena kodenya jelek, melainkan karena kami masih memelihara pola pikir lama: memperlakukan koneksi WebSocket persisten layaknya *request-response* HTTP konvensional yang dilayani oleh PHP-FPM di balik Apache atau Nginx.

Di dunia nyata, ketika ribuan pengguna aktif membuka tab browser, setiap koneksi WebSocket mengunci satu slot proses atau *thread* sistem operasi jika salah memilih teknologi dasar. Pendekatan tradisional seperti *polling* atau *long-polling* berulang kali menghantam web server dengan *overhead* header HTTP, membebani DNS, memicu pembacaan disk yang tidak perlu, dan membuat mesin kewalahan bahkan sebelum logika bisnis sempat dieksekusi. Kita butuh cara pandang baru yang membuang jauh-jauh beban *heavyweight thread* dan menggantinya dengan sesuatu yang jauh lebih ringan. Dari sinilah petualangan kami dimulai: meninggalkan batasan lama dan merangkul konkurensi tingkat dewa menggunakan ekosistem modern.

## 2. Di Balik Layar: Bagaimana Sistem Ini Bekerja Tanpa Sihir

Bayangkan sebuah gedung perkantoran besar dengan ribuan tamu yang ingin mengobrol secara bersamaan. Jika setiap tamu dijaga oleh satu orang resepsionis khusus (model *thread-per-connection*), gedung itu akan langsung kolaps karena kehabisan ruangan. Golang datang membawa solusi cerdas bernama Goroutines—bukan *OS thread* berat, melainkan *green thread* ringan yang hanya memakan memori awal sekitar 2KB saja. Satu mesin virtual Go bisa menangani ratusan ribu Goroutines secara bersamaan tanpa membuat kernel sistem operasi megap-megap.

Namun, masalah baru muncul ketika aplikasi kita harus berskala horizontal (*scale-out*) karena satu instance server Golang tidak lagi sanggup menampung 500.00 만 koneksi aktif. Ketika Client A terhubung ke Node 1 dan Client B terhubung ke Node 2, bagaimana caranya agar pesan dari A bisa sampai ke B secara instan? Di sinilah Redis Pub/Sub bertindak sebagai pusat saraf atau *neural network* tak terlihat. Redis menjadi jembatan pesan lintas server yang memastikan setiap event disiarkan secara *broadcast* ke seluruh instance Golang dalam hitungan milidetik, menciptakan ilusi ruang obrolan tunggal meskipun server fisik di balik layar berjumlah puluhan.

## 3. Pilihan Pendekatan & Kompromi yang Harus Diambil

Sebelum mengetik baris kode pertama, kita dihadapkan pada persimpangan jalan arsitektural yang menentukan nasib sistem dalam jangka panjang. Apakah kita harus menggunakan pustaka pihak ketiga yang instan, atau membangun *WebSocket hub* murni dari nol? 

Pendekatan pertama, menggunakan framework web instan seperti Node.js dengan Socket.io, memang sangat cepat untuk *prototype*, namun manajemen memorinya sering kali menjadi kotak hitam ketika menghadapi *garbage collection pauses* berskala masif. Pendekatan kedua, menggunakan erlang/Elixir, menawarkan ketahanan luar biasa namun memiliki kurva belajar tim yang sangat curam.

Kami akhirnya menjatuhkan pilihan pada perpaduan Golang murni dengan pustaka `gorilla/websocket` (atau versi standarnya yang kini makin matang) dikawinkan dengan Redis Pub/Sub. Komprominya? Kami harus menulis manajemen state secara manual dan merancang *in-memory hub* sendiri. Tapi imbalannya setimpal: performa mentah yang buas, konsumsi RAM yang sangat deterministik, dan kontrol penuh atas setiap siklus hidup koneksi *socket* tanpa ada sihir tersembunyi dari framework.

## 4. Meracik Lingkungan: Persiapan & Prasyarat Lapangan

Sebelum menyelam ke dalam kode, pastikan amunisi tempur di mesin lokal atau server *staging* sudah siap. Kita membutuhkan lingkungan pengembangan yang bersih:

- Go versi 1.21 atau lebih baru (mendukung peningkatan performa *map* dan *generic*).
- Redis Server versi 7.x yang berjalan pada port default `6379`.
- Nginx sebagai *reverse proxy* terdepan yang bertugas melakukan *upgrade header* HTTP ke WebSocket.

Mari kita siapkan struktur direktori proyek yang rapi dan modular:

```bash
mkdir -p real-time-go/{cmd/server,internal/hub,internal/redis}
cd real-time-go
go mod init github.com/username/realtime-go
```

Instal dependensi esensial yang kita butuhkan untuk menangani protokol WebSocket dan klien Redis:

```bash
go get github.com/gorilla/websocket
go get github.com/redis/go-redis/v9
```

> [!IMPORTANT]
> Pastikan batas *file descriptor* (`ulimit -n`) di sistem operasi Linux Anda sudah dinaikkan minimal ke angka `65535`. Secara default, sistem operasi sering kali membatasi jumlah koneksi terbuka hanya 1024, yang akan langsung membuat server WebSocket Anda lumpuh seketika saat menerima lonjakan trafik awal.

## 5. Langkah Implementasi: Membangun Solusi yang Tangguh

Mari kita mulai merangkai jantung dari aplikasi ini. Kita buat sistem *Hub* di dalam Golang yang bertugas melacak seluruh klien yang terhubung, mendata *channel* percakapan, dan mendistribusikan pesan masuk secara aman menggunakan *Channels*.

Berikut adalah implementasi `internal/hub/hub.go`:

```go
package hub

import (
	"encoding/json"
	"log"
)

type Message struct {
	Room    string `json:"room"`
	Content string `json:"content"`
}

type Client struct {
	ID   string
	Send chan []byte
	Room string
}

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
			log.Printf("Klien terhubung: %s", client.ID)
		case client := <-h.Unregister:
			if _, ok := h.Clients[client]; ok {
				delete(h.Clients, client)
				close(client.Send)
				log.Printf("Klien terputus: %s", client.ID)
			}
		case message := <-h.Broadcast:
			for client := range h.Clients {
				select {
				case client.Send <- message:
				default:
					close(client.Send)
					delete(h.Clients, client)
				}
			}
		}
	}
}
```

Kode di atas menggunakan pola *actor model* sederhana khas Go, di mana interaksi antar Goroutines diatur sepenuhnya melalui *channels* tanpa kunci mutex manual yang rentan terhadap *deadlock*.

## 6. Membedah Baris Kode Krusial yang Kerap Menjebak

Bagian paling krusial dan sering menjadi sumber malapetaka di produksi adalah integrasi Redis Pub/Sub dengan *Hub* Golang kita agar mampu melintasi batas multi-node server. Ketika sebuah instance menerima pesan, pesan tersebut harus dipublikasikan ke Redis, lalu didengar kembali oleh seluruh instance (termasuk dirinya sendiri) untuk disebarkan ke klien lokal.

Mari kita lihat bagaimana `internal/redis/broker.go` menangani sinkronisasi ini:

```go
package redis

import (
	"context"
	"encoding/json"
	"log"
	"github.com/redis/go-redis/v9"
	"github.com/username/realtime-go/internal/hub"
)

type RedisBroker struct {
	Client *redis.Client
	PubSub *redis.PubSub
}

func NewRedisBroker(addr string) *RedisBroker {
	rClient := redis.NewClient(&redis.Options{
		Addr: addr,
	})
	return &RedisBroker{Client: rClient}
}

func (r *RedisBroker) Publish(ctx context.Context, channel string, msg hub.Message) error {
	data, err := json.Marshal(msg)
	if err != nil {
		return err
	}
	return r.Client.Publish(ctx, channel, data).Err()
}

func (r *RedisBroker) Subscribe(ctx context.Context, channel string, h *hub.Hub) {
	r.PubSub = r.Client.Subscribe(ctx, channel)
	ch := r.PubSub.Channel()

	go func() {
		for msg := range ch {
			// Teruskan pesan dari Redis pub/sub ke lokal broadcast hub
			h.Broadcast <- []byte(msg.Payload)
		}
	}()
}
```

> [!TIP]
> Jangan pernah melakukan operasi *blocking* di dalam *goroutine listener* Redis tanpa menyediakannya saluran penampung (*channel buffer*). Jika *broadcast channel* penuh, proses mendengarkan pesan dari Redis akan macet dan memicu *backpressure* yang merambat ke memori server Redis.

## 7. Bertahan di Bawah Tekanan: Skenario Traffic Nyata

Bayangkan skenario di mana 50.000 pengguna aktif mulai mengirimkan *heartbeat* atau *ping-pong frame* setiap 30 detik untuk menjaga koneksi tetap hidup melalui load balancer Nginx. Di sinilah konfigurasi Nginx memegang peran penyelamat nyawa.

Jika Nginx tidak dikonfigurasi dengan benar untuk mendukung koneksi *long-polling* dan *upgrade header*, koneksi WebSocket Anda akan terputus secara misterius setiap 60 detik karena *timeout* diam-diam oleh *proxy reverse*.

Berikut adalah konfigurasi Nginx production-ready yang wajib Anda terapkan pada blok server:

```nginx
upstream websocket_backend {
    server 127.0.0.1:8080;
    # Tambahkan instance backend lain untuk load balancing horizontal
    # server 127.0.0.1:8081;
}

server {
    listen 80;
    server_name chat.domain.com;

    location /ws {
        proxy_pass http://websocket_backend;
        proxy_http_version 1.1;
        
        # Header krusial untuk melakukan handshake WebSocket
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        
        # Menaikkan timeout agar koneksi long-living tidak terputus prematur
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}
```

## 8. Jebakan Klasik & Cara Menghindarinya Sebelum Meledak

Berdasarkan pengalaman bertahun-tahun mengawal infrastruktur skala besar, ada tiga jebakan mematikan yang kerap menjebak *engineer* pemula maupun menengah saat membangun sistem WebSocket:

1. **Goroutine Leak (Kebocoran Goroutine):** Terjadi ketika klien terputus secara paksa (misal: kehilangan sinyal seluler), namun pembaca (*reader loop*) pada Goroutine di server tidak mendeteksi penutupan *socket* dan terus membaca *channel* yang menggantung selamanya. Selalu pastikan Anda memanggil fungsi pembacaan dengan batas waktu atau *ping/pong handler* bawaan dari pustaka WebSocket.
2. **Ketiadaan Backpressure Management:** Ketika satu klien mengalami lambat jaringan, *send channel*-nya akan menumpuk pesan hingga menghabiskan RAM server. Solusinya, beri kapasitas *buffer* terbatas pada *channel* klien dan putuskan koneksi secara elegan jika *buffer* tersebut meluber (*overflow*).
3. **Koneksi Redis Terputus Tanpa Auto-Reconnect:** Pastikan klien Redis dikonfigurasi dengan *pool* koneksi yang sehat dan penanganan kesalahan (*error handling*) yang memadai agar aplikasi tidak mengalami *panic* saat Redis melakukan *failover* atau *restart*.

## 9. Uji Nyali: Mengukur Performa di Titik Batas

Jangan pernah percaya bahwa kode Anda siap sebelum disiksa di bawah tekanan ekstrem. Kami biasa menggunakan utilitas modern seperti `k6` atau `websocat` untuk melakukan *stress testing* secara lokal atau di lingkungan *staging*.

Buat skrip uji sederhana menggunakan `k6` (`load_test.js`):

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

export const options = {
  stages: [
    { duration: '30s', target: 1000 }, // Naikkan ke 1000 pengguna dalam 30 detik
    { duration: '1m', target: 5000 },  // Tahan di 5000 pengguna aktif
    { duration: '10s', target: 0 },    // Turunkan perlahan
  ],
};

export default function () {
  const url = 'ws://localhost:8080/ws';
  const res = ws.connect(url, {}, function (socket) {
    socket.on('open', function () {
      socket.send('Halo dari load test k6!');
    });
    socket.on('message', function (message) {
      // Verifikasi pesan masuk
    });
    socket.on('close', function () {});
  });

  check(res, { 'status is 101': (r) => r && r.status === 101 });
}
```

Jalankan pengujian dan pantau penggunaan CPU serta RAM menggunakan perintah `htop` atau `go tool pprof` untuk mendeteksi potensi alokasi memori yang berlebihan.

## 10. Catatan Lapangan & Checklist Sebelum Rilis

Sebelum Anda menekan tombol *merge* ke cabang `main` dan meluncurkan sistem ini ke lingkungan *production*, luangkan waktu sejenak untuk memeriksa daftar periksa (*checklist*) operasional berikut:

- [ ] Apakah batas *file descriptor* sistem operasi (`ulimit -n`) sudah ditingkatkan?
- [ ] Apakah Nginx sudah dikonfigurasi dengan direktif `proxy_read_timeout` yang memadai?
- [ ] Apakah mekanisme *ping/pong* WebSocket sudah diaktifkan di sisi server dan klien untuk membersihkan *zombie connection*?
- [ ] Apakah Redis Pub/Sub sudah diuji ketahanan jaringannya saat terjadi *reconnect* otomatis?
- [ ] Apakah metrik pemantauan (seperti jumlah koneksi aktif, penggunaan memori Goroutine, dan latensi pesan) sudah diintegrasikan ke sistem *monitoring* seperti Prometheus dan Grafana?

Jika seluruh kotak di atas sudah tercentang, selamat! Anda kini memiliki arsitektur real-time web berbasis Golang dan Redis yang tidak hanya cepat, tetapi juga tangguh menghadapi badai trafik di dunia nyata.