# Real-Time Event-Driven Architecture dengan NATS dan WebSockets

> Diterbitkan oleh Literally Web ID pada 20 Oct 2026
> Sumber: https://literally.web.id/articles/real-time-event-driven-architecture-dengan-nats-dan-websockets

---

# Real-Time Event-Driven Architecture dengan NATS dan WebSockets

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

Ingatkah Anda malam ketika traffic melonjak tiga kali lipat karena flash sale mendadak, dan dasbor pemantauan tiba-tiba berubah menjadi lautan warna merah menyala? Di ruang server, CPU pada instance backend Go kita melompat ke 95%, bukan karena payload datanya besar, melainkan karena ratusan ribu koneksi WebSocket yang terbuka secara bersamaan saling berebut memori dan membuat *file descriptor* meluber. Tim frontend mengeluh data harga tidak sinkron, sementara tim infrastruktur sibuk mematikan server karena kehabisan RAM.

Masalah klasik ini sering kali berakar dari kesalahan mendesain komunikasi real-time. Mengandalkan *long polling* HTTP atau membiarkan setiap instance server web mengelola koneksi WebSocket secara terisolasi tanpa message broker yang handal adalah resep bencana. Ketika aplikasi diskalakan secara horizontal menjadi lima node di belakang load balancer Nginx, Node A tidak tahu apa yang sedang didengar oleh Node B. Pesan yang dikirimkan oleh pengguna di Node A tidak pernah sampai ke klien yang terhubung melalui Node B. Di sinilah kita membutuhkan arsitektur terdistribusi berbasis event yang mampu menyatukan jutaan kabel virtual tanpa membuat peladen kolaps.

Transisi dari arsitektur monolitik yang reaktif menuju sistem event-driven yang tangguh menuntut kita untuk tidak hanya memikirkan kode yang berjalan di mesin lokal, tetapi juga bagaimana data mengalir melintasi batas-batas klaster server yang terdistribusi secara geografis. Mari kita bedah bagaimana fondasi ini dibangun dari nol.

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

Untuk memahami cara kerja sistem real-time berlatensi rendah, bayangkan NATS sebagai sistem pos kilat super cepat di dalam sebuah pabrik raksasa, sementara WebSocket adalah saluran pipa transparan yang langsung menghubungkan tangan kurir ke meja pelanggan. Klien web membuka koneksi TCP abadi melalui WebSocket ke sebuah *Gateway*—dalam hal ini, aplikasi Go ringan yang bertindak sebagai jembatan.

> [!TIP]
> Jangan pernah membiarkan klien WebSocket berbicara langsung ke database atau memproses logika berat. Gunakan *Gateway* yang hanya bertugas melakukan enkapsulasi bingkai (*framing*) dan meneruskannya ke message broker.

Ketika sebuah event terjadi di sistem—misalnya tombol "beli" ditekan—backend mengirimkan pesan JSON ke subjek NATS tertentu, katakanlah `orders.checkout.success`. NATS Server, yang ditulis dalam bahasa Go dan terkenal karena efisiensinya yang mendekati logam (*bare metal*), secara instan merutekan pesan tersebut ke seluruh *Gateway* yang berlangganan pada subjek tersebut. *Gateway* kemudian menerjemahkan pesan NATS tadi menjadi bingkai teks WebSocket dan mendorongnya ke ribuan browser dalam hitungan milidetik. Tanpa polling, tanpa beban I/O disk, murni operasi memori asinkron.

Namun, memilih komponen yang tepat di atas kertas sering kali berbeda jauh dengan realita pengujian beban di lingkungan staging. Mari kita lihat kompromi apa saja yang harus kita ambil.

## 3. Pilihan Pendekatan & Kompromi yang Harus Diambil

Sebelum mengetikkan baris kode pertama, kita dihadapkan pada persimpangan jalan arsitektur. Apakah kita harus menggunakan Redis Pub/Sub, Apache Kafka, atau NATS Server? Mari kita buang teori pemasaran dan lihat faktanya di lapangan.

Redis Pub/Sub sangat cepat, tetapi ia memiliki kelemahan fatal: *fire-and-forget* murni tanpa jaminan pengiriman (*at-most-once*). Jika klien terputus sedetik saja, pesan hilang selamanya. Di sisi lain, Apache Kafka adalah raksasa penyimpanan persisten yang luar biasa untuk *event sourcing* skala enterprise, namun kompleksitas operasionalnya—membutuhkan ZooKeeper (atau KRaft baru-baru ini) dan pengelolaan partisi disk yang ketat—terlalu berlebihan jika tujuan kita hanya mengalirkan notifikasi UI instan.

NATS hadir sebagai jalan tengah yang sempurna. Ia menawarkan kecepatan memori Redis dengan keandalan perutean yang dirancang khusus untuk komunikasi mikroservis dan edge computing. NATS JetStream, fitur persistensi bawaannya, bahkan memberikan kita kemampuan jaminan pengiriman pesan (*at-least-once*) tanpa overhead manajemen klaster yang menyiksa.

> [!IMPORTANT]
> Pilih NATS Core untuk komunikasi ephemeral (data transien seperti posisi kursor atau indikator mengetik) dan aktifkan JetStream hanya untuk data kritikal finansial atau status pesanan yang tidak boleh hilang.

Dengan keputusan arsitektur yang sudah bulat, saatnya kita menyiapkan tangan untuk meracik lingkungan pengembangan lokal.

## 4. Meracik Lingkungan: Persiapan & Prasyarat Lapangan

Kita tidak akan menggunakan Docker Compose yang membengkak dengan puluhan layanan tak dikenal. Kita butuh setup yang bersih, deterministik, dan mendekati produksi. Kita akan menjalankan NATS Server mandiri dan aplikasi Gateway berbasis Go.

Pastikan Anda memiliki Go (versi 1.21 atau lebih baru) dan NATS CLI terinstal di mesin Anda. Mari kita mulai dengan menginisialisasi direktori proyek:

```bash
mkdir -p nats-ws-gateway/cmd/gateway
mkdir -p nats-ws-gateway/internal
cd nats-ws-gateway
go mod init github.com/username/nats-ws-gateway
```

Selanjutnya, unduh dependensi esensial kita: pustaka klien NATS resmi dan `gorilla/websocket` untuk menangani protokol WebSocket di sisi Go.

```bash
go get github.com/nats-io/nats.go
go get github.com/gorilla/websocket
```

Untuk menjalankan NATS Server secara lokal untuk keperluan pengembangan, kita bisa memanfaatkan Docker dengan konfigurasi port standar:

```yaml
version: '3.8'
services:
  nats:
    image: nats:2.10-alpine
    container_name: nats-server
    ports:
      - "4222:4222"
      - "8222:8222"
    command: ["--jetstream", "-m", "8222"]
```

Jalankan kontainer tersebut dengan `docker compose up -d`. Sekarang server NATS dengan fitur JetStream aktif di port `4222`, dan dasbor pemantauan HTTP siap di port `8222`. Mari kita tulis kode jembatannya.

## 5. Langkah Implementasi: Membangun Solusi yang Tangguh

Inti dari sistem ini adalah *Gateway* yang bertindak ganda: sebagai klien NATS yang mendengarkan topik internal, dan sebagai server WebSocket yang melayani koneksi dari browser pengguna.

Mari buat berkas `cmd/gateway/main.go` dengan struktur yang bersih dan penanganan konkurensi yang aman:

```go
package main

import (
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"

	"github.com/gorilla/websocket"
	"github.com/nats-io/nats.go"
)

var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	CheckOrigin: func(r *http.Request) bool {
		// Di produksi, validasi origin dengan ketat!
		return true
	},
}

func main മുഖ്യമന്ത്രി() {}

func main() {
	// Koneksi ke klaster NATS
	natsURL := os.Getenv("NATS_URL")
	if natsURL == "" {
		natsURL = nats.DefaultURL
	}

	nc, err := nats.Connect(natsURL)
	if err != nil {
		log.Fatalf("Gagal terhubung ke NATS: %v", err)
	}
	defer nc.Close()
	log.Println("Berhasil terhubung ke NATS broker")

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

		// Setiap klien berlangganan ke subjek universal atau spesifik pengguna
		sub, err := nc.Subscribe("events.broadcast", func(msg *nats.Msg) {
			err := conn.WriteMessage(websocket.TextMessage, msg.Data)
			if err != nil {
				log.Printf("Gagal mengirim pesan ke klien WebSocket: %v", err)
			}
		})
		if err != nil {
			log.Printf("Gagal membuat langganan NATS untuk klien: %v", err)
			return
		}
		defer sub.Unsubscribe()

		// Blok eksekusi agar koneksi tetap hidup sampai klien putus
		for {
			_, _, err := conn.ReadMessage()
			if err != nil {
				break
			}
		}
	})

	server := &http.Server{Addr: ":8080"}
	go func() {
		log.Println("Gateway WebSocket berjalan pada port :8080")
		if err := server.ListenAndServe(); err != http.ErrServerClosed {
			log.Fatalf("HTTP server error: %v", err)
		}
	}()

	// Graceful shutdown
	stop := make(chan os.Signal, 1)
	signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
	<-stop
	log.Println("Mematikan gateway secara elegan...")
}
```

Kode di atas terlihat sederhana, namun ada beberapa detail implementasi mendalam yang kerap menjebak para insinyur di lapangan. Mari kita bedah bagian tersebut.

## 6. Membedah Baris Kode Krusial yang Kerap Menjebak

Perhatikan bagaimana langganan NATS (`nc.Subscribe`) dibuat di dalam penanganan HTTP handler WebSocket. Setiap kali klien membuka satu koneksi baru, satu langganan NATS baru dibuat di dalam broker. Apakah ini buruk?

Untuk skala di bawah 10.000 klien per node, NATS menangani langganan subjek liar (*wildcard subscriptions*) dengan sangat efisien. Namun, jika Anda melayani 100.000 klien per instans Gateway, membuat langganan NATS per koneksi akan membebani tabel perutean memori NATS.

> [!WARNING]
> Jangan membuat *dedicated NATS subscription* per koneksi WebSocket jika Anda mengantisipasi ratusan ribu klien konkuren pada satu instance *Gateway*. Gunakan *Hub Pattern* terpusat di dalam memori aplikasi Go, di mana *Gateway* hanya memiliki **satu** langganan NATS global, lalu menyebarkan pesan masuk ke seluruh klien WebSocket yang aktif melalui peta (*map*) aman-benang (*thread-safe*).

Selain manajemen memori, tantangan terbesar berikutnya muncul ketika jaringan internet pengguna berkedip—situasi yang sangat lumrah di dunia nyata.

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

Di dunia nyata, kabel jaringan tidak pernah sempurna. Pengguna seluler melewati terowongan kereta bawah tanah, beralih dari Wi-Fi kantor ke data seluler 5G, atau mengalami gangguan transmisi sesaat. Jika *Gateway* tidak menangani siklus hidup koneksi putus-sambung (*reconnection*) dengan benar, memori server akan mengalami kebocoran (*memory leak*) secara perlahan karena *goroutine* yang menggantung menunggu tulis jaringan yang macet.

Untuk mengatasinya, implementasikan mekanisme *Ping/Pong heartbeat* secara rutin pada level WebSocket:

```go
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
conn.SetPongHandler(func(string) error {
	conn.SetReadDeadline(time.Now().Add(60 * time.Second))
	return nil
})
```

Jika klien tidak merespons *ping* dalam waktu 60 detik, koneksi diputus secara paksa oleh server, sumber daya memori dibersihkan, dan langganan NATS yang terkait langsung dibatalkan (*unsubscribed*). Pendekatan defensif ini memastikan server Anda tetap bernapas meskipun diguyur badai koneksi yatim piatu.

Namun, bahkan dengan kode yang paling kokoh sekalipun, ada beberapa jebakan klasik yang sering kali lolos dari pengujian lokal.

## 8. Jebakan Klasik & Cara Menghindarinya Sebelum Meledak

Kesalahan paling sering yang dilakukan pengembang pemula pada arsitektur berbasis event adalah mengabaikan urutan pesan (*message ordering*) dan penanganan duplikasi data.

Ketika jaringan terputus dan klien melakukan *reconnect*, klien tersebut mungkin melewatkan sepuluh event penting yang terjadi saat mereka offline. Jika Anda menggunakan NATS Core biasa, pesan-pesan tersebut hilang selamanya bagi klien tersebut. Solusinya adalah memadukan NATS JetStream dengan mekanisme *Consumer Sequence ID* di sisi klien. Ketika klien terhubung kembali, mereka mengirimkan *last_seen_id* terakhir mereka ke Gateway, dan Gateway meminta replay pesan dari JetStream KV store atau JetStream Consumer sebelum melanjutkan aliran real-time biasa.

Jebakan kedua adalah kurangnya pembatasan laju (*rate limiting*). Seorang pengguna usil dapat memicu ratusan event dalam sedetik dari sisi klien, membanjiri NATS, dan melumpuhkan seluruh klaster. Selalu pasang *token bucket rate limiter* pada titik masuk Gateway sebelum pesan dipublikasikan ke NATS.

Sebelum meluncurkan sistem ini ke produksi, mari kita uji seberapa jauh batas kemampuan arsitektur ini.

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

Jangan pernah percaya bahwa sistem Anda siap sebelum diuji di bawah tekanan ekstrem. Kita dapat menggunakan alat pengujian beban modern seperti `k6` atau utilitas khusus WebSocket untuk mensimulasikan ribuan klien virtual.

Buat skrip pengujian k6 sederhana (`load-test.js`):

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

export const options = {
  stages: [
    { duration: '30s', target: 1000 }, // Naikkan ke 1000 klien dalam 30 detik
    { duration: '1m', target: 1000 },  // Tahan beban selama 1 menit
    { 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 () {
      console.log('Klien terhubung');
    });
    socket.on('message', function (message) {
      check(message, { 'pesan diterima': (msg) => msg.length > 0 });
    });
    socket.setTimeout(function () {
      socket.close();
    }, 45000);
  });

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

Jalankan pengujian menggunakan perintah `k6 run load-test.js` sambil memantau penggunaan CPU dan memori melalui dasbor NATS di port `8222`. Anda akan takjub melihat bagaimana satu instans Go kecil mampu menangani ribuan koneksi dengan konsumsi memori yang tidak lebih dari beberapa puluh megabita.

## 10. Catatan Lapangan & Checklist Sebelum Rilis

Sebelum Anda menekan tombol *merge* terakhir dan mendorong kode ini ke lingkungan produksi, pastikan checklist operasional berikut sudah terpenuhi:

- [ ] **Konfigurasi NATS Cluster**: Jalankan minimal 3 node NATS untuk mencapai kuorum tinggi (*fault tolerance*).
- [ ] **File Descriptor Limits**: Tingkatkan batas sistem operasi (`ulimit -n`) pada server Gateway Anda agar tidak mengalami galat `too many open files` saat trafik melonjak.
- [ ] **Load Balancer Configuration**: Pastikan Nginx atau AWS ALB di depan Gateway Anda dikonfigurasi dengan *timeout* WebSocket yang memadai (misalnya `proxy_read_timeout 3600s`) dan mengaktifkan dukungan *connection upgrade*.
- [ ] **TLS Enkripsi**: Gunakan WSS (`wss://`) alih-alih WS biasa di produksi menggunakan sertifikat Let's Encrypt yang dikelola secara otomatis oleh Caddy atau Certbot.
- [ ] **Observabilitas**: Hubungkan metrik NATS Prometheus ke Grafana untuk memantau *inbound/outbound message rate* secara *real-time*.

Arsitektur real-time bukan tentang seberapa pintar kode yang Anda tulis, melainkan seberapa tangguh sistem tersebut berdiri ketika kekacauan terjadi di dunia nyata. Selamat membangun sistem yang tidak hanya cepat, tetapi juga tahan banting!