Banyak developer pernah galau harus nambah Redis Streams, NATS, atau Kafka cuma buat kebutuhan notifikasi real-time — padahal database yang udah jalan cuma Postgres? Tenang, kondisi ini umum terjadi. Untuk mayoritas workload skala kecil-menengah, Postgres sebenernya udah punya primitive bawaan yang sering dilupakan: LISTEN dan NOTIFY.
Dan kabar baiknya: primitive ini bukan cuma "bisa dipakai" — benchmark terbaru nunjukin ia melaju jauh lebih kencang dari perkiraan umum. Tim DBOS mempublikasikan pengukuran yang menunjukkan 60.000 writes per detik pada satu server Postgres dengan latensi milidetik untuk aliran data berbasis LISTEN/NOTIFY, dan benchmark resmi milis pgsql-hackers ngasih angka tps yang bisa jadi patokan jujur. Artikel ini bedah cara kerja, batasan, pola production yang bener, angka benchmark nyata, dan kapan waktunya pindah ke broker.
Kenapa LISTEN/NOTIFY Sering Dilewatkan
Stigma-nya jelas: LISTEN/NOTIFY dianggap "jadul", "cuma buat notifikasi sederhana", atau "gak scalable". Padahal ini fitur yang udah ada sejak Postgres 7.x dan terus dirawat — primitive inti yang jalan di dalam engine yang sama dengan transaksi pengguna.
Keuntungan paling kentara: nol infrastruktur tambahan. Gak ada broker baru yang harus di-deploy, di-monitor, di-backup, dan dibayar. Kalau pengguna sudah berjalan di atas Postgres, LISTEN/NOTIFY cuma butuh satu baris SQL. Untuk workload yang gak butuh replay, DLQ, atau fan-out masif, nambah Kafka cuma demi "sok scalable" = cara paling mahal bikin sistem makin kompleks.
Bagi yang sedang mencari VPS murah buat ngetes arsitektur kayak gini tanpa jor-joran, RackNerd punya paket yang cukup buat ngejalanin Postgres + worker kecil-kecilan — hemat dulu sebelum mikir nambah broker.
Cara Kerja LISTEN/NOTIFY
Di level paling dasar, NOTIFY channel_name, 'payload' ngirim pesan ke semua sesi Postgres yang lagi LISTEN channel_name. Pesan ini bukan disimpan di WAL untuk dikonsumsi nanti — ia didistribusikan lewat shared memory di dalam instance Postgres yang sama. Implikasinya tegas: LISTEN/NOTIFY cuma bekerja di dalam satu cluster Postgres, gak lintas node, kecuali terhubung ke sistem eksternal.
Payload dibatasi sampai 8000 byte (batas halaman WAL). Dua aturan semantik yang sering bikin developer kaget:
- Pesan dikirim pas COMMIT, bukan per statement. Semua NOTIFY dalam satu transaksi baru dieksekusi saat commit. Jadi transaksi panjang = notifikasi telat.
- Dedupe per transaksi. Kalau pengguna kirim NOTIFY dengan channel + payload yang identik berkali-kali dalam satu transaksi, Postgres cuma ngirim SATU pesan.
Karena payload mentok 8000 byte, idiom paling umum adalah thin notification: kirim identifier (UUID atau row ID), lalu listener query tabel buat ambil detail lengkap. Pola ini praktik terbaik karena:
- Throughput tinggi — payload kecil berarti lebih banyak notifikasi per detik
- Anti-staleness — listener selalu baca state terbaru dari tabel, gak pernah pegang data basi
- Schema flexibility — perubahan struktur tabel gak ngerusak kontrak payload
Batasan yang Wajib Dipahami
Sebelum memaksakan LISTEN/NOTIFY buat production, ada beberapa batas keras yang harus diterima apa adanya:
- Gak ada replay built-in. Kalau gak ada listener aktif pas NOTIFY dipicu, pesan ilang selamanya. Buat event penting yang gak boleh ilang, outbox pattern adalah satu-satunya jalan aman.
- Gak ada ordering global. Urutan pesan dijamin per channel dalam satu instance, tapi gak ada jaminan ordering kalau ada beberapa writer ke channel yang sama.
- Single instance only. LISTEN/NOTIFY gak mereplikasi pesan ke replica atau read replica. Buat multi-node, replikasi manual atau external broker diperlukan.
- Resource amplification. Setiap listener nambah overhead CPU dan memory di server Postgres. Fan-out ke puluhan ribu listener itu masalah nyata — angkanya disebut di bagian benchmark.
LISTEN/NOTIFY vs Redis Streams vs Kafka: Kapan Pakai Apa
Ini perbandingan jujur — gak ada yang menang mutlak, semua tergantung kebutuhan:
| Aspek | Postgres LISTEN/NOTIFY | Redis Streams | Kafka / NATS |
|---|---|---|---|
| Durability & replay | Gak ada replay; pesan ilang tanpa listener | Persisten sampai di-trim manual | Persisten dengan retention policy |
| Fan-out | Linear per listener — bottleneck utama | Baik, consumer group bawaan | Dirancang khusus buat fan-out masif |
| Ordering | Per channel, satu instance | Per stream, per consumer group | Per partition |
| Kompleksitas operasional | Nol — udah ada di Postgres | Satu service tambahan, perlu backup + monitoring | Cluster multi-node, rebalancing, disk rakus |
| Kapan cocok | Notifikasi UI, cache invalidation, workload < 10K event/detik | Queue sederhana, pub/sub ringan | Event sourcing, audit trail, > 100K event/detik, multi-region |
Intinya: butuh replay, DLQ, atau fan-out ke puluhan sistem sekaligus? Itu sinyal buat broker. Cuma butuh "kasih tahu worker ada data baru"? Postgres udah cukup.
Pola 1: Thin Notification + Outbox (At-Least-Once)
Pola paling aman dan paling gampang di-debug. Saat ada perubahan di tabel bisnis, trigger atau aplikasi nulis baris ke tabel outbox dalam transaksi yang sama. Worker kemudian SELECT baris yang belum diproses, ngejalanin NOTIFY, dan nandain baris sebagai terkirim. Ini ngejamin notifikasi terkirim setidaknya sekali (at-least-once) dan aman dipakai idempotent kalau listener pake deduplication key.
-- Schema outbox
CREATE TABLE event_outbox (
id BIGSERIAL PRIMARY KEY,
aggregate_id UUID NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
notified_at TIMESTAMPTZ
);
CREATE INDEX idx_outbox_pending ON event_outbox (id) WHERE notified_at IS NULL;
-- Trigger setelah insert ke tabel order
CREATE OR REPLACE FUNCTION notify_order_event() RETURNS TRIGGER AS $$
BEGIN
INSERT INTO event_outbox (aggregate_id, event_type, payload)
VALUES (NEW.id, TG_ARGV[0]::TEXT, to_jsonb(NEW));
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER order_created AFTER INSERT ON orders
FOR EACH ROW EXECUTE FUNCTION notify_order_event('order.created');
Sisi worker (Python + asyncpg):
# worker.py
import asyncio
import asyncpg
import json
async def run():
conn = await asyncpg.connect(DATABASE_URL)
queue = asyncio.Queue()
def handle_notify(_conn, _pid, channel, payload):
queue.put_nowait(json.loads(payload))
await conn.add_listener('orders', handle_notify)
while True:
event = await queue.get()
# dispatch ke downstream (websocket, search index, dst.)
await dispatch(event)
asyncio.run(run())
Pas worker reconnect setelah down, ia tinggal query baris outbox dengan notified_at IS NULL buat ngejar event yang kelewat — itu sebabnya outbox ngejamin at-least-once, sementara direct NOTIFY gak bisa.
Pola 2: Trigger-Based Direct NOTIFY
Pola lebih sederhana, cocok buat workload volume rendah-menengah di mana kehilangan notifikasi pas worker restart masih bisa diterima. Trigger langsung manggil pg_notify('channel', json_build_object(...)).
CREATE OR REPLACE FUNCTION emit_change() RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify(
'table_change',
json_build_object(
'op', TG_OP,
'table', TG_TABLE_NAME,
'id', NEW.id
)::text
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Kode-nya jauh lebih sedikit, tapi tanpa jaminan at-least-once. Kalau worker mati antara NOTIFY dan eksekusi downstream, event ilang. Buat use case idempotent kayak cache invalidation, pola ini aman. Buat event yang memicu aksi bisnis penting, wajib pindah ke pola outbox.
Pola 3: Logical Replication untuk Multi-Node
Buat deployment multi-region atau gabungan beberapa instance Postgres ke satu event stream, kombinasi logical replication + listener adalah opsi menarik. Output plugin pgoutput mempublikasikan perubahan sebagai event berformat standar, yang bisa di-decode listener lalu diteruskan ke sistem lain (misal Kafka). Ini beda dari LISTEN/NOTIFY murni karena datanya beneran ngalir lewat WAL ke node lain — bukan cuma sinyal in-memory.
Setup-nya lebih kompleks dan biasanya baru masuk akal kalau memang butuh replikasi event lintas region. Untuk single-region, pola outbox atau direct NOTIFY udah lebih dari cukup. Bagi yang ingin mengulik Postgres lebih dalam buat skenario data analytics, tersedia panduan arsitektur LTAP Postgres + Parquet buat data analytics yang bisa jadi referensi lanjutan.
Benchmark Nyata: Berapa Jauh LISTEN/NOTIFY Bisa Melaju
Lupakan angka "50K-100K notifikasi per detik" yang gak jelas sumbernya — ada dua sumber yang bisa dicek sendiri. Pertama, DBOS (tim yang ngembangin Postgres sebagai runtime aplikasi) mempublikasikan artikel "Postgres LISTEN/NOTIFY Can Actually Scale", menunjukkan aliran data berbasis LISTEN/NOTIFY yang di-optimize sampai 60.000 writes per detik di satu server Postgres dengan latensi milidetik.
Kedua, ada benchmark resmi dari milis pgsql-hackers (attachment pada diskusi "LISTEN/NOTIFY scalability benchmark") yang jalan di Ubuntu 24.04.2 LTS di atas UTM VM di Apple M3 Max 128 GB RAM. Ini angka tps-nya (pgbench, durasi 60 detik):
Skenario tps
------------------------------------------
1 NOTIFY -> 1 LISTEN 11.544,9
100 NOTIFY -> 1 LISTEN 7.494,3
1 NOTIFY -> 100 LISTEN 798,1
100 NOTIFY -> 100 LISTEN 1.314,3
100 NOTIFY -> 100 LISTEN
(per-client channel) 1.419,3
Catatan penting: angka ini dari VM di laptop (M3 Max), bukan server production dedicated — jadi anggap ini baseline konservatif. Di hardware yang lebih kencang, angkanya bakal lebih tinggi, persis kayak yang DBOS tunjukin.
Fan-Out Adalah Bottleneck Utama
Lihat baris ketiga tabel di atas: naikin listener 1→100 bikin throughput anjlok dari 11.544 tps ke 798 tps — turun sekitar 14,5×. Ini bukan angka receh, ini sinyal arsitektur yang jelas: fan-out adalah biaya utama LISTEN/NOTIFY.
Kenapa bisa begitu? Profil perf dari benchmark pgsql-hackers nunjukin dua titik dominan:
- Kasus 1→1: biaya didominasi fungsi
AtCommit_Notifyyang manggil syscallkill()buat nge-signal backend listener. - Kasus fan-out: biaya pindah ke
socket_flush/secure_write— nulis salinan pesan ke socket tiap listener.
Menariknya, varian 100→100 dengan channel per-client (1.419 tps) menang atas shared channel (1.314 tps). Artinya fan-out per-channel lebih murah daripada broadcast satu channel ke semua client. Rekomendasi praktisnya: kalau ada banyak worker yang masing-masing cuma butuh subset event, pakai channel terpisah per client daripada satu channel yang dibroadcast ke semua.
60K writes/s dari DBOS bukan kebetulan — mereka nerapin optimasi yang persis menyerang bottleneck ini (channel-per-client, payload tipis, ngurangin sinyal gak perlu). LISTEN/NOTIFY bisa scale, tapi fan-out-nya harus didesain sadar.
Operasional: Heartbeat dan Reconnect
Ada beberapa catatan operasional yang sering diabaikan sampai production:
Heartbeat channel buat connection health. Jalanin NOTIFY heartbeat, 'tick' tiap 5 detik dari scheduled job; worker yang gak nerima heartbeat dalam 15 detik dianggap putus dan harus reconnect. Ini mencegah worker diam-diam berhenti nerima event gara-gara network blip.
Reconnect dengan exponential backoff. Library kayak asyncpg gak otomatis reconnect setelah network bermasalah. Bungkus listener dalam loop reconnect dengan backoff 1s, 2s, 4s, 8s, 16s, cap 30s. Pas reconnect, ambil baris outbox yang belum diproses sejak timestamp terakhir worker hidup buat ngejar event yang kelewat.
Monitoring Queue Depth
Query berikut ngasih visibility yang bagus ke kesehatan pipeline:
SELECT
EXTRACT(EPOCH FROM (NOW() - MIN(created_at))) AS oldest_pending_seconds,
COUNT(*) AS pending_count
FROM event_outbox
WHERE notified_at IS NULL;
Aturan alert-nya simpel:
- Alert kalau
oldest_pending_secondslewat 60 detik — tanda worker lambat atau down. - Alert kalau
pending_counttumbuh tanpa batas — tanda worker gak mampu ngejar volume.
Kapan Pindah ke Message Broker
Ada sinyal jelas yang nunjukin LISTEN/NOTIFY udah gak memadai dan butuh broker khusus:
- Butuh event replay buat rekonstruksi state atau audit — pakai Kafka atau NATS JetStream
- Ada lebih dari satu writer region dan event harus di-merge dari semuanya
- Throughput yang dibutuhin konsisten di atas 100.000 event/detik
- Ada kebutuhan dead-letter queue dan retry dengan backoff eksponensial
- Event harus didistribusikan ke sistem yang gak punya koneksi langsung ke Postgres (mobile push, third-party webhooks)
Untuk kasus di luar batas itu — mayoritas aplikasi bisnis, SaaS kecil-menengah, dashboard internal, sistem notifikasi — LISTEN/NOTIFY ngasih rasio kesederhanaan/kapabilitas yang susah dikalahkan. Prinsip "pakai yang udah ada sampai ada bukti butuh lebih" juga dibahas di artikel optimasi SQLite untuk aplikasi production.
Checklist Sebelum Migrasi ke Broker
Sebelum pindah ke broker, jawab ini dulu:
- Volume notifikasi per detik? Di bawah 10K, LISTEN/NOTIFY lebih dari cukup; 10K-50K masih bisa tuning; konsisten di atas 100K, baru serius pertimbangin broker.
- Event penting gak boleh ilang (audit, compliance)? Kalau iya, outbox adalah satu-satunya cara aman di LISTEN/NOTIFY.
- Berapa downstream yang nerima event? 1-3 simpel; lebih dari 5, pertimbangin broker yang emang dirancang buat fan-out.
- Butuh replay event historis? LISTEN/NOTIFY gak support built-in — outbox table harus nyimpen history selama durasi yang dibutuhkan.
- Gimana operasi multi-region? Listener di region beda kena latency propagasi; cross-region = logical replication atau broker geo-distributed.
Checklist ini bukan aturan keras — tiap proyek punya constraint sendiri, tapi premis yang bagus: kalau belum yakin butuh message broker, mungkin memang tidak butuh.
FAQ Seputar LISTEN/NOTIFY
Q: Payload maksimal berapa? Kalau lebih, gimana?
Maksimal 8000 byte (batas halaman WAL). Lebih dari itu, jangan maksa — kirim ID-nya aja (thin notification) dan biarin listener yang query detailnya.
Q: Notifikasi hilang kalau gak ada listener pas NOTIFY dipicu?
Iya, ilang selamanya. LISTEN/NOTIFY gak punya replay built-in. Kalau event-nya penting, wajib lewat outbox pattern supaya ada jaminan at-least-once — worker yang telat bisa ngejar dari tabel outbox.
Q: Bedanya sama logical replication apa?
LISTEN/NOTIFY itu sinyal in-memory dalam satu instance — cepet, tapi gak persisten dan gak lintas node. Logical replication (pgoutput) ngirim perubahan lewat WAL ke node lain dan persisten.
Q: Aman dipakai production gak?
Aman, selama batasannya dipahami: pake outbox buat event penting, heartbeat buat deteksi worker mati, dan jangan fan-out ke ratusan listener tanpa desain channel yang bener. Ribuan aplikasi production udah jalan di atas pola ini. Bagi yang baru mulai bangun backend di atas Postgres, disarankan membaca dulu panduan lengkap REST API Go + Postgres dari nol biar fondasinya beres sebelum nambah layer real-time.
Q: Kapan wajib pindah ke broker?
Lima sinyal: butuh replay/audit, multi-writer-region, konsisten > 100K event/detik, butuh DLQ + retry backoff, atau fan-out ke sistem tanpa koneksi Postgres (push mobile, webhook). Selain itu, LISTEN/NOTIFY cukup.
Kesimpulan
Postgres LISTEN/NOTIFY adalah fitur berusia puluhan tahun yang tetap relevan karena filosofinya yang sederhana: kirim notifikasi saat state berubah, biarin listener yang mutusin. Benchmark terbaru ngebuktiin ia bisa nanganin puluhan ribu event per detik kalau fan-out-nya didesain bener — DBOS aja nembus 60K writes/s di satu server. Pola outbox ngasih jaminan at-least-once buat event penting, direct NOTIFY ngasih kesederhanaan buat kasus idempotent, logical replication buka jalan ke multi-node. Migrasi ke Redis Streams, NATS, atau Kafka baru masuk akal pas batasan di atas beneran kelewat — bukan sebagai default arsitektur. Mulai dari yang sudah ada, ukur dulu, baru nambah komponen kalau ada bukti.
Bagi yang mau ngetes pola ini di VPS murah tanpa mikir biaya broker tambahan, RackNerd bisa jadi starting point yang oke — satu VPS cukup buat Postgres + worker, dan bisa scale nanti kalau emang butuh.
Sumber dan Validasi
Artikel ini disusun berdasarkan dokumentasi resmi PostgreSQL dan benchmark yang bisa diverifikasi langsung:
- DBOS — Postgres LISTEN/NOTIFY Can Actually Scale (Peter Kraft, 2026)
- pgsql-hackers — LISTEN/NOTIFY scalability benchmark (pgbench, 60s)
- PostgreSQL Documentation — NOTIFY
- PostgreSQL Documentation — LISTEN
- ElectricSQL — Sync Benchmarks Reference
Catatan: Artikel ini disusun berdasarkan dokumentasi resmi dan benchmark publik yang tercantum di atas. Angka pgsql-hackers bersifat spesifik terhadap hardware pengujian (UTM VM di Apple M3 Max, 128 GB RAM, Ubuntu 24.04.2) dan hanya baseline konservatif, bukan patokan absolut. Angka 60K writes/s dari klaim publik tim DBOS (2026). Selalu validasi terhadap workload dan hardware sendiri sebelum mengambil keputusan arsitektur.
💬 Komentar (0)
Belum ada komentar. Jadilah yang pertama! 💬