# Membangun Real-time Notification Pipeline dengan Event-Driven Architecture

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

---

## 1. Alarm Tengah Malam: Saat Layanan Produksi Mulai Tumbang

Pukul 02.14 WIB. Ponsel saya bergetar liar dengan nada alarm PagerDuty paling menyebalkan yang pernah ada—nada yang didesain khusus untuk merusak siklus REM tidur siapa pun. Di layar, metrik Grafana menyala merah menyala seperti lampu diskotik darurat. Latency endpoint WebSocket kami melompat dari 45 milidetik menjadi 12.000 milidetik dalam kurun waktu kurang dari tiga menit. Ribuan koneksi klien mulai terputus secara massal, diikuti oleh banjir keluhan dari tim *Customer Success* di kanal Slack internal.

Sistem kami, sebuah platform notifikasi real-time yang didesain untuk mendistribusikan *event* jutaan pengguna, mendadak mengalami *bottleneck* parah. Broker RabbitMQ kami melaporkan *memory alarm* menyala, menandakan bahwa antrean pesan (*queues*) meluber hingga menelan habis seluruh alokasi RAM yang tersedia. Di sisi lain, backend Node.js yang menangani koneksi klien WebSocket mulai mengalami *heap out of memory* karena menumpuk payload yang gagal terkirim ke browser. Malam itu, kami tidak sedang membangun fitur baru; kami sedang berperang melawan entropi infrastruktur skala besar.

Perjalanan investigasi kami malam itu dimulai dari ruang kendali digital, menelusuri jejak digital yang ditinggalkan oleh tumpukan log yang berdarah-darah.

## 2. Jejak Digital: Menelusuri Anomali Log & Lonjakan Metrik

Begitu mendarat di terminal, hal pertama yang saya lakukan adalah membuka beberapa tab SSH ke klaster server produksi menggunakantmux. Saya langsung memantau status RabbitMQ menggunakan CLI bawaannya untuk melihat apa yang sebenarnya terjadi di balik layar:

```bash
# Memeriksa status node RabbitMQ dan penggunaan memori secara real-time
sudo rabbitmqctl status
sudo rabbitmqctl list_queues name messages memory consumers
```

Angkanya mengerikan. Antrean utama dengan nama `notification.fanout` memiliki lebih dari 4,2 juta pesan yang belum diproses (*unacked*), sementara jumlah *consumer* aktif turun drastis dari 64 instance menjadi hanya 3 instance saja. Sisa instance backend Node.js kami ternyata mengalami *crash* beruntun akibat kehabisan memori (*OOM Killer* kernel Linux menyapu bersih proses yang memakan RAM lebih dari 90%).

Kami beralih memeriksa log Nginx yang bertindak sebagai *reverse proxy* dan SSL termination di depan server WebSocket:

```nginx
# Konfigurasi upstream Nginx untuk WebSocket dengan timeout yang ketat
upstream websocket_backend {
    server 10.0.1.101:3000 max_fails=3 fail_timeout=10s;
    server 10.0.1.102:3000 max_fails=3 fail_timeout=10s;
}

server {
    listen 443 ssl http2;
    server_name ws.production.io;

    location / {
        proxy_pass http://websocket_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 60s;
        proxy_send_timeout 60s;
    }
}
```

Dari log Nginx, terlihat jelas pola serangan atau lonjakan trafik yang memicu bencana ini: sebuah *flash sale* dari salah satu merchant besar menyebabkan lonjakan *event* pendaftaran pesanan sebanyak 50 kali lipat dari rata-rata normal. Masalahnya, payload event tersebut tidak memiliki skema versioning yang jelas, dan struktur datanya membengkak hingga 15 KB per pesan karena menyertakan metadata yang tidak perlu. Klien WebSocket di sisi pengguna akhir, yang sebagian besar berada di jaringan seluler lambat, tidak mampu mengonsumsi data secepat itu. Akibatnya, terjadilah fenomena *backpressure* fatal yang merembet ke seluruh ekosistem backend.

## 3. Membongkar Biang Kerok: Akar Masalah yang Sebenarnya

Setelah menggali lebih dalam melalui analisis *core dump* dan log aplikasi Node.js, kami menemukan tiga dosa besar arsitektural yang kami perbuat sendiri pada fase desain sistem:

1. **Desain Event Payload yang Monolitik:** Kami mengirimkan seluruh objek *state* database ke dalam broker pesan tanpa melakukan *projection* atau *versioning*. Ketika struktur tabel berubah, konsumen lama langsung mengalami *parsing error*, dan ukuran payload membunuh efisiensi jaringan.
2. **Kurangnya Mekanisme Backpressure:** Layanan WebSocket kami berasumsi bahwa semua klien memiliki kecepatan internet gigabit dan kemampuan proses yang tak terbatas. Ketika klien lambat menerima data, server terus membuang pesan ke dalam buffer TCP lokal hingga memori aplikasi habis.
3. **Konfigurasi RabbitMQ yang Naif:** Kami menggunakan *default exchange* tanpa menerapkan *dead-letter exchange* (DLX) yang layak, serta tidak membatasi ukuran antrean (*queue length limits*), sehingga broker menerima pesan tanpa batas hingga kehabisan RAM.

Untuk memperbaiki ini secara permanen, kita perlu merombak total cara kita mendesain payload, mengelola antrean broker, dan menangani klien yang lambat.

## 4. Tindakan Darurat: Menghidupkan Kembali Layanan dalam Hitungan Menit

Sambil menyusun rencana jangka panjang, sistem harus segera diselamatkan. Berikut adalah rangkaian perintah darurat *one-liner* terminal yang kami eksekusi dalam waktu kurang dari 60 detik untuk meredakan tekanan pada klaster:

```bash
# 1. Melakukan purge pada antrean yang macet total untuk menyelamatkan broker dari OOM
sudo rabbitmqctl purge_queue notification.fanout

# 2. Melakukan restart paksa pada seluruh klaster Node.js WebSocket melalui PM2
pm2 reload all --update-env

# 3. Menjatuhkan koneksi TCP yang menggantung menggunakan ss dan iptables jika diperlukan
sudo ss -K 'sport = :443' state established
```

Langkah drastis ini langsung menurunkan penggunaan memori RabbitMQ dari 98% ke level aman 22%. Namun, ini baru pertolongan pertama. Agar tidak tumbang lagi, kami harus membangun fondasi baru yang tahan banting.

## 5. Kebijakan Retrospektif: Menambal Celah agar Tak Pernah Terulang

Belajar dari tragedi malam itu, kami merombak arsitektur dengan menerapkan standar ketat pada tiga pilar utama: skema payload, konfigurasi broker, dan manajemen backpressure.

### Desain Event Payload dan Skema Versioning

Setiap event yang masuk ke dalam sistem kini wajib mengikuti kontrak skema yang jelas dengan menyertakan header versi. Berikut adalah contoh implementasi payload terstruktur dalam format JSON yang bersih dan efisien:

```json
{
  "specversion": "1.0",
  "type": "com.production.notification.order.created",
  "source": "/orders-service",
  "id": "A234-5678-90AB-CDEF",
  "time": "2023-10-25T02:30:00Z",
  "datacontenttype": "application/json",
  "dataversion": "v2",
  "data": {
    "order_id": 987654,
    "user_id": 4321,
    "status": "PAID",
    "total_amount": 150000
  }
}
```

Dengan format ini, konsumen dapat melakukan validasi menggunakan pustaka seperti Ajv di Node.js atau JSON Schema validator lainnya sebelum memproses pesan lebih lanjut.

### Konfigurasi Exchange & Queue di RabbitMQ

Kami membuang konfigurasi *default* dan beralih menggunakan *Topic Exchange* yang dikawinkan dengan *Dead-Letter Exchange* (DLX) serta pembatasan kapasitas antrean (*max-length*):

```go
// Contoh deklarasi exchange dan queue yang aman di Go (amqp091-go)
package main

import (
	"log"
	"github.com/streadway/amqp"
)

func SetupRabbitMQ(ch *amqp.Channel) error {
	// Deklarasi Dead Letter Exchange untuk menampung pesan gagal
	err := ch.ExchangeDeclare(
		"dlx.exchange", "direct", true, false, false, false, nil,
	)
	if err != nil { return err }

	// Deklarasi Queue Utama dengan argumen pembatas
	args := amqp.Table{
		"x-max-length":     100000,
		"x-overflow":       "reject-publish",
		"x-dead-letter-exchange": "dlx.exchange",
		"x-dead-letter-routing-key": "dead.notifications",
	}

	_, err = ch.QueueDeclare(
		"secure.notification.queue", true, false, false, false, args,
	)
	return err
}
```

### Menangani Backpressure pada WebSocket Client yang Lambat

Di sisi backend Node.js, kami menerapkan manajemen buffer berbasis ukuran (*highWaterMark*) dan mekanisme *heartbeat* yang agresif untuk mendeteksi klien yang mandeg (*zombie clients*):

```javascript
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 3000 });

wss.on('connection', (ws) => {
    ws.isAlive = true;
    ws.on('pong', () => { ws.isAlive = true; });

    ws.sendEvent = (payload) => {
        // Cek apakah buffer socket hampir penuh (mencegah memory leak)
        if (ws.bufferedAmount > 65536) { // 64KB threshold
            console.warn('Backpressure terdeteksi, menutup koneksi klien lambat...');
            ws.terminate();
            return;
        }
        ws.send(JSON.stringify(payload));
    };
});

// Interval pengecekan kesehatan koneksi
const interval = setInterval(() => {
    wss.clients.forEach((ws) => {
        if (ws.isAlive === false) return ws.terminate();
        ws.isAlive = false;
        ws.ping();
    });
}, 30000);
```

## 6. Pasang Alarm: Otomasi Deteksi Dini untuk Tim On-Call

Agar insiden serupa tidak lagi menyelinap diam-diam di tengah malam, kami memasang aturan peringatan (*alerting rules*) baru di Prometheus dan Alertmanager yang terintegrasi langsung dengan webhook PagerDuty:

```yaml
groups:
  - name: notification_pipeline_alerts
    rules:
      - alert: RabbitMQQueueHighMessages
        expr: rabbitmq_queue_messages{queue="secure.notification.queue"} > 50000
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Antrean RabbitMQ menumpuk tinggi!"
          description: "Jumlah pesan dalam antrean melewati batas aman 50.000 selama lebih dari 2 menit."

      - alert: WebSocketHighBufferedAmount
        expr: nodejs_websocket_buffered_amount_bytes > 1048576
        for: 1m
        labels:
          severity: warning
        annotations:
          summary: "Potensi Backpressure pada WebSocket Server"
          description: "Buffer pengiriman data WebSocket melebihi 1MB per instance."
```

Dengan sistem pemantauan proaktif ini, kami kini bisa tidur dengan lebih nyenyak, mengetahui bahwa infrastruktur event-driven kami tidak hanya cepat, tetapi juga tangguh menghadapi badai trafik apa pun.