Zendoric
← Back to the day · July 28, 2026

Postgres LISTEN/NOTIFY does scale: DBOS hits 60,000 writes per second with low-latency streaming

🕒 Published on Zendoric: July 28, 2026 · 00:38

DBOS has published a technical article seeking to rehabilitate the reputation of LISTEN/NOTIFY, PostgreSQL's native pub/sub mechanism, which a widely cited earlier post accused of "not scaling".

DBOS has published a technical article that seeks to rehabilitate the reputation of LISTEN/NOTIFY, PostgreSQL's native pub/sub mechanism, which a widely cited earlier post accused of "not scaling". The post, written by Peter Kraft and published on July 24, does not deny the underlying problem, but explains it in detail and shows how to avoid it: with the right optimizations, their LISTEN/NOTIFY-based streams implementation reaches 60,000 writes per second on a single Postgres server, with millisecond latencies.

The use case that motivates the analysis is low-latency streaming: for example, delivering a language model's response token by token. The basic design is simple —a "streams" table where each new chunk is an inserted row— but reading that flow is the hard part, because the reader does not know when the next chunk will arrive. The obvious alternative, polling, forces a choice between two evils: a long polling interval introduces too much latency for interactive cases like a live chat, and a short one saturates the database with concurrent readers. LISTEN/NOTIFY solves this in theory, because it lets readers block waiting for a notification instead of actively polling, waking up immediately when new data arrives.

DBOS's first implementation used a trigger on the streams table that fired a notification every time a new chunk was written. It worked correctly and with low latency, but its throughput was poor: even with a large database, it could not sustain more than 2,900 stream writes per second. Strikingly, this bottleneck appeared without any visible consumption of Postgres resources —no CPU, no memory, no IOPS— a sign that the problem was not capacity but contention.

The article devotes its meatiest section to explaining the exact origin of that contention: committing a transaction that calls NOTIFY requires taking a global exclusive lock. That lock is acquired when the commit begins and is not released until the transaction is fully committed, including the flush to disk via fsync(). The reason for this design is that Postgres guarantees notifications are delivered in the same order in which the transactions that generated them committed: internally it maintains a global queue of outgoing notifications whose order must exactly match commit order, and adding items to that queue has to happen transactionally as part of the commit itself. The problem is that Postgres does not assign a commit order to a transaction until it finishes committing —and that process can take a variable amount of time— which creates an ordering paradox: you need to know the commit order to insert into the queue, but that order does not exist until the commit concludes. Postgres's solution is precisely the global lock: it serializes the commits of all transactions that include notifications, so their order is fixed in advance.

Since the original trigger fired NOTIFY on every stream write, each of those writes had to take the global lock and hold it for the entire commit, disk included. This forced writes to commit strictly sequentially, preventing Postgres from applying standard optimizations such as group commit (batching many transactions into a single fsync()). Throughput was then capped by the speed of committing transactions one at a time, which explains both the ceiling of 2,900 writes per second and the absence of visible CPU or disk usage: everything was serialized by a lock, not by a lack of machine capacity.

The text adds a relevant note about a Postgres patch, slated for version 19, that has generated some public discussion on this same topic. According to DBOS, that patch does not remove the global lock or resolve the bottleneck described: it merely optimizes a narrower case, the one in which there are many distinct notification channels and each listener waits on only one specific channel. It is a useful clarification, to avoid overestimating the scope of the changes coming in future versions of the engine.

The solution DBOS proposes starts from a conceptual observation: in streams —and in many other uses of LISTEN/NOTIFY— the notification itself is not the source of truth, but simply a nudge for the reader to check the database table, where the actual data lives. As a result, notifications do not need to be perfectly ordered or absolutely durable, which opens the door to optimizing them: instead of issuing one NOTIFY per write, they are accumulated in an in-memory buffer and flushed periodically in a single batched transaction. This drastically reduces contention on the global lock, because it is only taken when the buffer is flushed and not on every individual write. Individual stream writes can then commit quickly, taking advantage of group commit again, while the buffer flush happens in the background.

This strategy does introduce a new complication: if the process crashes while notifications are sitting unflushed in the buffer, those notifications are lost forever. DBOS solves this with a fallback mechanism on the reader side: in addition to waiting for notifications, readers periodically poll the database to check whether the stream received writes that were never notified. Since this polling is only a safety net for exceptional cases, its frequency can be low and it barely affects overall performance.

With this optimized version, the numbers improve substantially: up to 60,000 stream writes per second with active concurrent readers —twenty times more than the initial implementation— while maintaining latencies of between 15 and 100 milliseconds. At peak throughput, Postgres's CPU is fully utilized, which for DBOS is proof that the system is now genuinely saturated by real work rather than blocked by artificial lock contention.

Beyond the technical detail, the article leaves a broader lesson for anyone building pub/sub or streaming infrastructure on Postgres: "counterintuitive" and poorly documented behavior —like NOTIFY's global lock— does not automatically amount to "does not scale", but it does require understanding the internal mechanism before discarding the tool or rushing into architectures with external queues and brokers (Kafka, Redis, etc.) when redesigning how notifications are emitted might be enough. DBOS publishes the benchmark code on GitHub (dbos-inc/dbos-postgres-benchmark) for anyone who wants to reproduce the numbers, which adds transparency to the reported figures, though it is worth remembering that they come from the vendor itself and not from an independent evaluation.

🔗 Related on Zendoric

Sources & references