# Membangun Real-Time Notification System dengan WebSockets dan Go

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

---

Dalam 15 menit ke depan, kita akan merakit sebuah sistem notifikasi real-time berstandar industri dari nol sampai berjalan mulus. Pengalaman di lapangan membuktikan bahwa menjaga 100.000 koneksi WebSocket konkuren agar tetap stabil bukanlah perkara sihir, melainkan soal bagaimana kita mengelola memori goroutine dan mencegah lonjakan garbage collection yang brutal. Sambil menyeruput kopi, bayangkan kita sedang berada di ruang kontrol menghadapi trafik flash sale e-commerce: ribuan client terhubung serentak, dan satu saja memory leak kecil di level handler akan membuat server kita bertekuk lutut dalam hitungan menit.

Mari kita bedah peta jalannya secara taktis. Kita tidak akan membuang waktu membahas teori klise tentang apa itu WebSocket. Sebaliknya, kita langsung fokus pada tiga pilar utama yang menentukan hidup-matinya arsitektur real-time: bagaimana kita mendistribusikan tugas melalui Goroutine Pool Management agar CPU tidak jebol, bagaimana kita membersihkan koneksi zombie menggunakan Heartbeat & Connection Cleanup, serta bagaimana kita menyambungkan Redis Pub/Sub Integration for Multi-Node Scaling saat aplikasi kita harus diskalakan secara horizontal di balik load balancer Nginx.

```bash
# Mari siapkan direktori kerja kita dan inisialisasi modul Go
mkdir -p real-time-engine && cd real-time-engine
go mod init notification-engine

# Unduh pustaka gorilla/websocket dan driver redis
go get github.com/gorilla/websocket
go get github.com/redis/go-redis/v9
```
Perhatikan respons terminal Anda setelah perintah di atas tuntas. Jika tidak ada error merah yang muncul, artinya fondasi Go environment kita sudah siap menerima beban kerja berat.

Sekarang kita masuk ke jantung permainan: Goroutine Pool Management dan penanganan koneksi. Setiap client WebSocket yang masuk akan memakan sebuah goroutine jika tidak diatur dengan bijak. Di sinilah kita meracik kode Go yang bersih, efisien, dan menggunakan struktur data channel untuk membatasi konkurensi serta mencegah OOM (Out of Memory) killer menyapa kernel Linux kita.

```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 },
}

type Client struct {
	hub  *Hub
	conn *websocket.Conn
	send chan []byte
}

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)
				}
			}
		}
	}
}
```

> [!IMPORTANT]
> Jangan pernah membiarkan penulisan ke socket dilakukan secara konkuren tanpa mutex atau channel buffer. Pola `select` di atas menjamin bahwa jika satu client lambat merespons, goroutine pengirim tidak akan mengalami hang (goroutine leak).

Agar koneksi yang terputus di tengah jalan tidak meninggalkan 'mayat' di memori RAM server, kita wajib menerapkan Heartbeat & Connection Cleanup. Koneksi TCP yang terputus karena kabel cabut atau HP kehilangan sinyal sering kali tidak langsung terdeteksi oleh OS. Di sinilah mekanisme ping-pong berkala menyelamatkan server kita dari kebocoran file descriptor.

```go
const (
	writeWait      = 10 * time.Second
	pongWait       = 60 * time.Second
	pingPeriod     = (pongWait * 9) / 10
	maxMessageSize = 512
)

func (c *Client) readPump() {
	defer func() {
		c.hub.unregister <- c
		c.conn.Close()
	}()
	c.conn.SetReadLimit(maxMessageSize)
	c.conn.SetReadDeadline(time.Now().Add(pongWait))
	c.conn.SetPongHandler(func(string) error {
		c.conn.SetReadDeadline(time.Now().Add(pongWait))
		return nil
	})

	for {
		_, _, err := c.conn.ReadMessage()
		if err != nil {
			if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
				log.Printf("error: %v", err)
			}
			break
		}
	}
}

func (c *Client) writePump() {
	ticker := time.NewTicker(pingPeriod)
	defer func() {
		ticker.Stop()
		c.conn.Close()
	}()

	for {
		select {
		case message, ok := <-c.send:
			c.conn.SetWriteDeadline(time.Now().Add(writeWait))
			if !ok {
				c.hub.unregister <- c
				c.conn.WriteMessage(websocket.CloseMessage, []byte{})
				return
			}

			w, err := c.conn.NextWriter(websocket.TextMessage)
			if err != nil {
				return
			}
			w.Write(message)

			n := len(c.send)
			for i := 0; i < n; i++ {
				w.Write([]byte{'
'})
				w.Write(<-c.send)
			}

			if err := w.Close(); err != nil {
				return
			}
		case <-ticker.C:
			c.conn.SetWriteDeadline(time.Now().Add(writeWait))
			if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
				return
			}
		}
	}
}
```

> [!TIP]
> Set nilai `ulimit -n` di OS Linux Anda menjadi minimal 200.000 agar kernel tidak menolak file descriptor baru saat ribuan client mulai handshaking bersamaan.

Ketika aplikasi Go kita sukses menampung 100.000 koneksi pada satu instance server, tantangan berikutnya muncul: bagaimana jika instance server kita bertambah menjadi tiga karena beban trafik melonjak? Di sinilah Redis Pub/Sub Integration for Multi-Node Scaling berperan sebagai jembatan komunikasi antar-node. Ketika Node A menerima event notifikasi, Redis akan menyiarkannya ke Node B dan Node C secara instan.

```go
package main

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

var ctx = context.Background()

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

> [!WARNING]
> Pastikan koneksi Redis menggunakan pool yang optimal dan aktifkan mekanisme reconnect otomatis. Jangan sampai kegabutan jaringan sesaat pada Redis memutus seluruh alur broadcast sistem notifikasi Anda.

Sentuhan akhir yang tidak boleh dilewatkan adalah optimasi sistem operasi dan Nginx sebagai reverse proxy. Tambahkan konfigurasi berikut pada blok server Nginx Anda agar koneksi WebSocket tidak terputus secara prematur oleh timeout bawaan:

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

    location /ws {
        proxy_pass http://127.0.0.1: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 3600s;
        proxy_send_timeout 3600s;
    }
}
```

Mari kita jalankan verifikasi kilat. Nyalakan server Go Anda dengan perintah `go run main.go`, lalu buka terminal terpisah dan gunakan perkakas `websocat` atau script kecil berbasis JavaScript di browser untuk melakukan koneksi uji coba.

```bash
# Uji koneksi cepat menggunakan websocat
websocat ws://localhost:8080/ws
```
Kirimkan sebuah teks dari terminal atau melalui publish Redis, lalu lihat apakah pesan tersebut langsung terpantul ke layar client tanpa jeda. Jika pesan muncul seketika, itu tanda bahwa arsitektur event-driven kita sudah berjalan sempurna di level produksi.

Kendala kecil yang sering ditemui di lapangan biasanya berupa error `too many open files`. Jangan panik, cukup sesuaikan parameter sistem limit di `/etc/security/limits.conf` dengan menambahkan baris `* soft nofile 200000` dan `* hard nofile 200000`, lalu restart layanan Anda. Selamat mencoba di environment staging Anda hari ini!