- Kunci eksklusif global pada Postgres LISTEN/NOTIFY membatasi throughput implementasi sederhana, tetapi dengan membuffer notifikasi dan mengirimnya secara batch, satu server dapat menangani hingga 60 ribu stream write per detik
- Transaksi yang memanggil NOTIFY menahan kunci global hingga commit dan
fsync()selesai untuk menjamin urutan commit notifikasi, sehingga commit terserialisasi dan group commit juga tidak bisa dimanfaatkan - Implementasi awal yang memanggil NOTIFY lewat trigger pada setiap penulisan ke tabel stream memberikan latensi rendah, tetapi mengalami bottleneck di 2.900 operasi per detik tanpa benar-benar memanfaatkan CPU, memori, dan IOPS
- Dengan menjadikan tabel database sebagai source of truth, bukan notifikasi, lalu mengirim notifikasi yang dikumpulkan di memori secara berkala dalam satu transaksi, jumlah pengambilan kunci bisa dikurangi drastis
- Kemungkinan notifikasi di buffer hilang akibat kegagalan proses ditutupi dengan polling berfrekuensi rendah, dan bahkan dalam lingkungan baca serentak tetap tercapai latensi 15~100ms serta throughput 20 kali lebih tinggi dibanding sebelumnya
Stream berlatensi rendah yang dibangun dengan Postgres
- Stream berbasis Postgres menyimpan setiap potongan stream sebagai baris baru di tabel
streams, dan token respons LLM pun bisa menjadi satu potongan - Di sisi pembaca, sulit menunggu secara efisien hanya dengan query biasa karena tidak diketahui kapan potongan berikutnya akan tiba
- Polling periodik menambah latensi untuk penggunaan interaktif seperti chat online jika intervalnya panjang, dan jika terlalu pendek maka poller serentak akan membanjiri database
- Dengan LISTEN/NOTIFY, proses pembaca bisa menunggu dalam keadaan blokir lalu langsung bangun ketika ada notifikasi bahwa potongan baru telah ditulis, sehingga polling yang tidak perlu bisa dihindari
Bottleneck dari NOTIFY yang dikirim pada setiap write
- Dalam implementasi awal, setiap kali potongan baru ditulis ke tabel
streams, trigger menjalankan fungsi untuk mengirim NOTIFY satu kali, lalu proses pembaca menunggu notifikasi sebelum membaca potongan baru - Akurasi dan latensi rendah memang tercapai, tetapi bahkan pada database Postgres yang besar pun tidak dapat mempertahankan lebih dari 2.900 stream write per detik
- Saat bottleneck terjadi, penggunaan CPU, memori, dan IOPS tidak terlihat tinggi, dan penyebabnya adalah kunci global pada jalur commit NOTIFY
Kunci global yang menjamin urutan commit
- Transaksi yang memanggil NOTIFY memperoleh kunci eksklusif global saat commit dimulai, dan tidak melepaskannya sampai commit selesai sepenuhnya dan isi transaksi ditulis ke disk melalui
fsync() - Postgres menjamin notifikasi dikirim sesuai urutan commit transaksi, dan menyimpan semua notifikasi keluar dalam antrean internal global yang harus persis sama dengan urutan commit
- Penambahan notifikasi ke antrean juga harus diproses secara transaksional sebagai bagian dari commit, tetapi karena waktu commit tiap transaksi berbeda, urutannya tidak bisa ditentukan sebelum commit selesai
- Kunci global membuat commit transaksi yang memuat notifikasi menjadi terserialisasi agar urutannya bisa dipastikan lebih dulu, dan ditambahkan ke antrean notifikasi internal dalam urutan yang sama
Bagaimana serialisasi commit membatasi throughput
- Karena semua stream write memanggil NOTIFY melalui trigger, setiap transaksi write menahan kunci global selama seluruh commit dan flush ke disk
- Karena transaksi di-commit satu per satu, group commit Postgres yang memproses beberapa transaksi dengan satu
fsync()tidak bisa dimanfaatkan - Throughput tidak bisa melampaui kecepatan Postgres meng-commit transaksi individual, dan pekerjaan menunggu pada kunci sehingga CPU serta disk juga tidak terpakai penuh
- Patch yang akan masuk ke Postgres 19 tidak menghapus kunci global, jadi tidak menyelesaikan bottleneck ini
- Sebagai gantinya, patch itu mengoptimalkan kasus terbatas dengan banyak channel notifikasi dan tiap listener hanya menunggu satu channel tertentu
Buffering notifikasi dan pengiriman batch
- Dalam berbagai penggunaan LISTEN/NOTIFY termasuk stream, notifikasi bukan source of truth, melainkan hanya sinyal untuk memeriksa tabel tempat data sebenarnya disimpan
- Dalam struktur seperti ini, notifikasi itu sendiri tidak perlu memiliki urutan global yang sempurna atau durabilitas penuh, sehingga bisa dibuffer di memori lalu dikirim berkala dalam satu transaksi batch
- Kunci global diambil bukan pada setiap stream write individual, melainkan hanya saat mengosongkan buffer
- Write individual dapat berjalan cepat karena terpisah dari pengiriman notifikasi latar belakang, dan throughput bisa ditingkatkan dengan memanfaatkan optimasi Postgres seperti group commit
Polling frekuensi rendah untuk menutupi notifikasi yang hilang
- Jika proses berhenti saat notifikasi masih berada di memori, notifikasi tersebut mungkin tidak terkirim
- Proses pembaca, sambil menunggu notifikasi, juga melakukan query ke database secara berkala untuk memeriksa apakah ada data stream yang ditulis tanpa notifikasi
- Karena polling ini hanyalah sarana pendukung untuk memulihkan notifikasi yang hilang, ia bisa dijalankan dengan frekuensi rendah dan dampaknya pada performa juga kecil
Throughput dan latensi
- Implementasi yang dioptimalkan dapat menangani hingga 60 ribu stream write per detik dalam lingkungan dengan proses pembaca serentak, mencatat throughput 20 kali lebih tinggi daripada implementasi awal
- Bahkan dengan throughput yang ditingkatkan, latensi tetap berada di kisaran 15~100ms
- Pada throughput maksimum, CPU Postgres terpakai penuh, menunjukkan bahwa sistem telah mencapai saturasi nyata database itu sendiri, bukan lagi contention pada kunci
- Seluruh kode benchmark dapat dilihat di dbos-postgres-benchmark
1 komentar
Komentar Hacker News
Skalabilitas adalah spektrum yang kontinu, bukan dikotomi. 60 ribu per detik bisa 100 ribu kali lebih banyak daripada kebutuhan untuk satu sistem, dan 100 ribu kali kurang untuk sistem lain. Jika harus menyebut kesalahan umum developer, saya akan memilih memilih teknologi dengan karakteristik skalabilitas yang tidak cocok, lebih daripada “optimisasi prematur”
Teknologi yang terlalu kecil memang jelas gagal ketika melewati batasnya, tetapi teknologi yang terlalu skalabel juga membawa beban operasional dan batasan. Mengadopsi teknologi seperti itu pada sistem kecil, yang sebenarnya bisa sangat mengurangi upaya pengembangan dengan model yang lebih kaya, juga merupakan pilihan buruk
Batas LISTEN/NOTIFY cukup rendah sehingga perlu diperhatikan; jadi setelah menghitung beban puncak secara pesimistis pun sebaiknya sisakan margin minimal 10x, tetapi untuk banyak proyek itu sudah memadai. Karena kelebihannya berupa integrasi dengan database, ketersediaan, dan tidak perlu mengoperasikan layanan terpisah, ini bukan opsi yang harus langsung disingkirkan; bahkan angka 2 ribu per detik yang sebelumnya disebutkan pun besar untuk sistem yang memproses satu pesan dalam hitungan detik
Lebih baik merancang berdasarkan skala yang benar-benar diperkirakan dengan tambahan kelonggaran yang wajar, dan memilih lebih dari itu hanya ketika skalabilitas tambahan pada dasarnya gratis. Jika bisa membeli hardware yang lebih besar dengan beberapa ribu dolar, atau pilihannya setara selain skalabilitas, pilih yang lebih besar
Saya pernah sangat sukses menggabungkan LISTEN/NOTIFY dengan broker langganan GraphQL berbasis Rust. Langganannya berjumlah puluhan ribu, tetapi koneksi LISTEN hanya satu per host, total 3–4 saja
Semua perubahan dikirim ke tiap host, lalu host mengelola langganan pengguna yang sebenarnya dan memutuskan apa yang akan dipublikasikan. Mengganti ratusan host Ruby atau Node dengan beberapa host Rust dapat sangat menyederhanakan arsitektur, dan pendekatan yang dianggap tidak skalabel pun bisa bekerja cukup baik
Saat bekerja sebagai CTO, semua layanan kami memproses sekitar 100 ribu kejadian per hari, lalu tumbuh menjadi jutaan, dan akhirnya puluhan juta. Dalam proses itu, seorang engineer membangun queue di atas semantik LISTEN/NOTIFY untuk memanfaatkan konsistensi kuat dengan model data. Itu tidak sulit dipahami, dan karena bisa menghilangkan lapisan penyimpanan serta pengiriman terpisah, saat itu tampak masuk akal
Namun ketika fitur buatan sendiri itu diskalakan, kami harus mengakali cara kerja internal PostgreSQL, yang menjadi sangat merepotkan, dan seharusnya kami pindah ke sistem lain lebih awal. Skalabilitasnya juga buruk; di RDS terjadi kontensi disk parah yang sulit dilacak penyebabnya, dan VACUUM pada tabel tersebut adalah mimpi buruk. Karena queue yang familiar diimplementasikan secara asing memakai fitur internal PostgreSQL, engineer lain merasa takut serta enggan melakukan debugging dan mengambil kepemilikan
Pelajaran inti, di luar detail seperti skema dan indeks, adalah selalu mulai dari teknologi yang sederhana dan dapat diprediksi. Jika tidak benar-benar membutuhkan konsistensi data yang sangat kuat, lebih baik memakai queue yang kontrak API-nya sederhana seperti SQS atau Redis queue, meskipun berarti menambah satu komponen infrastruktur, lalu menyesuaikan sisanya ke sana. Semakin sedikit tanggung jawab mekanis yang dipikul satu penyimpanan data inti, semakin baik
Seiring ekosistem sistem terdistribusi berkembang, kita makin memahami apa yang bisa dilakukan tiap komponen. Memulai dengan komponen yang sedikit lalu menambahkannya saat benar-benar dibutuhkan menghasilkan sistem yang lebih sehat
Saya terus menyukai DBOS yang memanfaatkan Postgres, dan kini juga SQLite, dengan benar. Ini bisa diperkenalkan ke stack CRUD yang sudah ada dengan hampir tanpa usaha
Begitu mulai memakai workflow yang durable, Anda akan terus melihat tempat untuk menerapkannya. Belakangan ini saya bereksperimen dengan memandang tiap email sebagai workflow yang durable, lalu membiarkan pengguna, pihak lawan, agent, serta alat seperti GitHub atau Attio ikut masuk ke alur secara bergiliran
https://housecat.com/blog/gmail-durable-workflows-sandbox-vm
Tulisan seperti ini umumnya merupakan hasil evaluasi independen atas masalah, pemahaman, dan solusi masing-masing. Agak sulit menyebutnya kurang ahli hanya karena mereka mengharapkan performa tertentu dari pengaturan default sebuah alat; semua orang terus belajar melalui kegagalan
Fakta bahwa eksperimen menggunakan server database 96 core·384GB RAM (https://github.com/dbos-inc/dbos-postgres-benchmark/blob/mai...) sangat penting dan seharusnya dinyatakan dengan jelas. Database memang bisa diskalakan secara vertikal, tetapi itu juga ada batasnya. Siapa yang terhubung dari mana juga memengaruhi performa dan latensi keseluruhan
60 ribu per detik mungkin tampak besar, tetapi yang merobohkan sistem nyata adalah lonjakan trafik sesaat, bukan trafik normal. Jika bukan perusahaan besar, Anda tidak akan memulai dengan server sebesar ini. Jika termasuk read replica dan redundansi lintas region, satu cluster database produksi bisa menelan biaya lebih dari 100 ribu dolar
Tampaknya artikel terkait: Postgres LISTEN/NOTIFY does not scale - https://news.ycombinator.com/item?id=44490510 - Juli 2025, 321 komentar
Sepertinya bagian terpenting dalam tulisan itu terlewat, yaitu cara menetapkan offset atau nomor urut yang memungkinkan pelacakan sampai sejauh mana konsumen sudah membaca dan mengambil pesan baru lewat jalur alternatif. Ada beberapa cara, tetapi tidak mudah diselesaikan tanpa kompleksitas atau kontensi lock, dan jika implementasinya salah bisa menimbulkan race condition dengan konsumen. Biasanya penulis akan mengunci satu baris di tabel status atau semacamnya untuk menetapkan nomor berikutnya bagi topik event
Saya penasaran apa cara terbaiknya. Tool yang membaca change data capture (CDC) lalu menulis nomor event yang ditetapkan ke tabel lain mungkin cukup baik, tetapi latensinya bisa menjadi tinggi, dan pemroses CDC itu juga harus melakukan NOTIFY
Di sisi konsumen, ada juga penggunaan yang bisa sangat meningkatkan throughput lewat batch processing. Dalam kasus ini, tanpa LISTEN/NOTIFY pun konsumen bisa dijalankan berulang untuk memproses semua pesan baru yang belum diproses setiap kali, lalu menyimpan nomor urut terakhir di antara iterasi
Sepingat saya, rilis yang pertama kali mendukung LISTEN/NOTIFY punya implementasi lock yang kurang baik sehingga ada masalah performa. Artikel lama yang dikritik di sini juga memperbaiki hal itu dalam koreksi tepat setelah paragraf pertama
Jika tanggal koreksinya 8 Mei, maka artikel 24 Juli perlu mengakui bahwa tulisan terkenal yang mengatakan fitur ini tidak dapat diskalakan bukan ditulis dengan niat buruk dan mungkin juga tidak salah untuk konteks saat itu
Sebaliknya, patch itu mengoptimalkan kasus yang lebih terbatas, ketika ada banyak channel notifikasi dan masing-masing penerima hanya menunggu satu channel tertentu
Terakhir kali saya cek, LISTEN/NOTIFY memiliki batas 8.000 byte pada data notifikasi, sehingga ada sisi yang jelas tidak skalabel. Jika datanya tidak bisa disimpan sebagai baris notifikasi dan hanya ID-nya yang dikirim, fitur ini sulit dipakai
Event pada game web adalah data sementara yang menjelaskan perubahan status, sehingga tidak ada alasan untuk menyimpannya di database, dan bisa saja melebihi 8.000 byte, jadi tidak cocok untuk penggunaan ini
Tulisan itu membahas kontensi lock pada antrean global, tetapi sepertinya tidak menyebut masalah lain dari antrean global berukuran tetap. Satu penerima lambat pada satu channel bisa memblokir penulisan ke semua channel. Setidaknya beberapa tahun lalu bentuk kegagalan seperti itu mungkin terjadi, meski sekarang mungkin sudah berubah