# Membangun Real-Time Notification Pipeline dengan WebSockets dan Go

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

---

Dalam 15 menit ke depan, kita akan merakit sebuah pipa notifikasi real-time berlatensi rendah dari nol sampai berjalan mulus, berbasis pengalaman di lapangan menangani lonjakan traffic jutaan pengguna. Lupakan teori textbook yang kaku; kita akan langsung menyelami bagaimana Go menangani ratusan ribu koneksi WebSocket secara bersamaan tanpa membuat RAM server Anda menjerit.

## 1. Menu Kita Hari Ini & Alat yang Perlu Disiapkan

Pernahkah Anda mendapati aplikasi web mendadak lumpuh saat ribuan pengguna terkoneksi secara bersamaan karena proses *garbage collection* Go tersendat atau manajemen goroutine yang buruk? Di dapur backend kali ini, kita tidak akan mengulang kesalahan itu. Kita akan menyusun sebuah sistem notifikasi tangguh menggunakan Go sebagai mesin utama WebSocket, Redis Pub/Sub sebagai lem perekat untuk *horizontal scaling* lintas server, dan sedikit trik pengaturan memori agar koneksi lama yang mangkir tidak menghabiskan sumber daya sistem.

> [!IMPORTANT]
> Pastikan Anda sudah menginstal Go versi 1.21 atau yang lebih baru, serta Redis server yang aktif di mesin lokal atau container Docker Anda sebelum kita mulai menyalakan kompor.

## 2. Peta Jalan Singkat: Apa yang Sedang Kita Rakit

Bayangkan kita sedang membangun dapur restoran cepat saji. Go bertindak sebagai koki utama yang melayani pelanggan secara langsung di meja lewat jalur WebSocket, sementara Redis Pub/Sub adalah pelayan pembawa pesan dari dapur pusat ke setiap cabang restoran agar tidak ada pesanan yang terlewat. Alur kerjanya sederhana: klien melakukan *handshake* WebSocket ke server Go, server mendaftarkan klien ke dalam *hub* lokal, dan ketika ada event baru, Redis akan menyiarkannya ke seluruh *instance* server Go yang sedang berjalan secara serentak.

Mari kita siapkan bahan-bahannya secara bertahap, mulai dari inisialisasi lingkungan kerja hingga pengujian beban secara langsung.

## 3. Langkah 1: Mempersiapkan Fondasi & Environment

Pertama-tama, buat direktori kerja baru di terminal Anda dan inisialisasi modul Go baru. Kita juga akan langsung menarik pustaka `gorilla/websocket` yang menjadi standar industri untuk menangani protokol WebSocket di Go.

```bash
mkdir real-time-pipeline && cd real-time-pipeline
go mod init notification-pipeline
go get github.com/gorilla/websocket
go get github.com/redis/go-redis/v9
```

Perhatikan respons dari perintah `go get` tersebut. Jika unduhan berjalan lancar tanpa error resolusi paket, itu tanda bahwa fondasi lingkungan kerja kita sudah siap dan stabil.

## 4. Langkah 2: Meracik Konfigurasi Inti Tanpa Basa-basi

Sekarang kita masuk ke bagian inti dapur kita. Buat sebuah file bernama `main.go`. Di sinilah kita merangkai logika `Hub` untuk mengelola klien WebSocket, serta integrasi Redis Pub/Sub agar arsitektur kita siap melakukan *scaling* secara horizontal di balik load balancer.

```go
package main

import (
	"context"
	"encoding/json"
	"log"
	"net/http"
	"time"

	"github.com/gorilla/websocket"
	"github.com/redis/go-redis/v9"
)

var ctx = context.Background()

// Upgrader standar untuk mengubah koneksi HTTP menjadi WebSocket
var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	CheckOrigin: func(r *http.Request) bool {
		return true // Sesuaikan untuk production agar membatasi origin
	},
}

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

// Hub mengelola sekumpulan client aktif
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)
				}
			}
		}
	}
}

func main() {
	hub := newHub()
	go hub.run()

	// Koneksi ke Redis untuk Pub/Sub Scaling
	rdb := redis.NewClient(&redis.Options{
		Addr: "localhost:6379",
	})

	// Subscribe ke channel Redis
	pubsub := rdb.Subscribe(ctx, "notifications")
	defer pubsub.Close()

	go func() {
		ch := pubsub.Channel()
		for msg := range ch {
			hub.broadcast <- []byte(msg.Payload)
		}
	}()

	http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
		conn, err := upgrader.Upgrade(w, r, nil)
		if err != nil {
			log.Println(err)
			return
		}
		client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
		hub.register <- client

		go client.writePump()
		go client.readPump()
	})

	log.Println("Server WebSocket berjalan di port :8080")
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}

func (c *Client) readPump() {
	defer func() {
		c.hub.unregister <- c
		c.conn.Close()
	}()
	for {
		_, _, err := c.conn.ReadMessage()
		if err != nil {
			break
		}
	}
}

func (c *Client) writePump() {
	ticker := time.NewTicker(54 * time.Second)
	defer func() {
		ticker.Stop()
		c.conn.Close()
	}()
	for {
		select {
		case message, ok := <-c.send:
			c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
			if !ok {
				c.conn.WriteMessage(websocket.CloseMessage, []byte{}))
				return
			}
			w, err := c.conn.NextWriter(websocket.TextMessage)
			if err != nil {
				return
			}
			w.Write(message)
			if err := w.Close(); err != nil {
				return
			}
		case <-ticker.C:
			c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
			if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
				return
			}
		}
	}
}
```

Perhatikan fungsi `writePump` di atas. Penggunaan *ticker* untuk mengirim `PingMessage` secara berkala adalah kunci utama agar koneksi tidak terputus di tengah jalan oleh *proxy* atau load balancer seperti Nginx yang agresif menutup koneksi idle.

## 5. Langkah 3: Sentuhan Akhir Keamanan & Optimasi

Di lingkungan produksi, Anda tidak bisa membiarkan koneksi terbuka tanpa batas. Pengaturan batas waktu (*timeouts*) dan ukuran buffer sangat menentukan performa jangka panjang. Pastikan juga Nginx di depan server Go Anda dikonfigurasi dengan benar untuk mendukung *Upgrade header* WebSocket.

```nginx
server {
    listen 80;
    server_name notif.domain.com;

    location /ws {
        proxy_pass http://localhost:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
    }
}
```

> [!TIP]
> Set nilai `proxy_read_timeout` menjadi `86400s` (24 jam) pada blok Nginx Anda agar koneksi WebSocket jangka panjang tidak terputus secara sepihak oleh timeout bawaan reverse proxy.

## 6. Tes Rasa: Memastikan Semuanya Berjalan Sempurna

Waktunya mencicipi hasil masakan kita! Jalankan server Go di terminal Anda:

```bash
go run main.go
```

Buka tab terminal lain, lalu gunakan CLI Redis untuk mempublikasikan pesan uji coba ke channel `notifications`:

```bash
redis-cli PUBLISH notifications "Halo dari Redis Pub/Sub!"
```

Jika Anda melihat pesan tersebut langsung mendarat di konsol klien WebSocket yang terhubung, selamat! Pipa real-time Anda telah beroperasi dengan sempurna dan siap mendistribusikan jutaan event.

## 7. Pertolongan Pertama: Jika Terjadi Kendala Kecil

> [!WARNING]
> Menghadapi error `websocket: close 1006 (abnormal closure)`? Ini biasanya terjadi karena jaringan klien terputus mendadak atau *idle timeout* di level load balancer terlalu singkat. Selalu pastikan mekanisme *ping/pong* berjalan aktif di level aplikasi dan naikkan batas *timeout* pada layer reverse proxy Anda.