Nitpick on an otherwise good post, but I don’t think there are very many 6billion RPS systems out there, and those that do exist are almost certainly using bespoke, purpose-built tools
They did send lawyers to my door in the end though.
Someone had forgot IPv6 rate limit bucketing :)
MQTT is everywhere, indeed, because of how simple the protocol is (it’s very ubiquitous in IoT / embedded systems).
Roughly a hundred servers on 100 GbE ports each could handle one small UDP packet per human every second, and would have a plenty of cycles and RAM per human left to actually do some work.
That fits in a shipping container.
Illustrative of course: the point is that "for every human" is not far out there given how crazy powerful hardware has become.
> Roughly a hundred servers on 100 GbE ports each could handle one small UDP packet per human every second ...
While there is no correlation between the Visa claim of 70k tps to people on the planet, there is also no correlation to the number of servers needed to process UDP packets.
The "Visa network" is more of a trusted collaboration between a small number of banks operating multi-step transactions than it is an OSI Layer 1 - 5 concern.
But then again it would still be no where near a million TPS.
What are these, TCP packets?
IMO one should design for actual anticipated scale with moderate margin, only exceeding this when it's relatively "free" to do so. (If you can buy bigger hardware for a few K, or if solutions are equivalent other than scalability, pick the bigger solution).
> At maximum throughput, Postgres CPU is fully utilized, showing the database is actually saturated instead of bottlenecked on contention.
As an example, spinlocks can push CPU usage very high whilst being an obvious symptom of contention.
In practice, this eventually ended up being very awkward because extending the functionality (since we had "built" it) and had to work around internal pg semantics (we should have just moved off much sooner). It also did not scale well. We ended up getting a ton of disk contention on our RDS instance in non-obvious ways, and the vacuum runs on that table was a nightmare. Additionally, it was hard to get other engineers to really debug and take ownership of the system because they automatically viewed a queue (very easy to understand) implemented in a foreign way (off pg internals) as something "scary". It was emotional, not rational, but we are emotional beings, and I do not blame them. These were good engineers with a lot of other things on their plates.
Obviously, this is all hand-wavy without discussing the internal schema, indeces, etc. that we had set up, but my main takeaway with core technology from this experience was to always reach for the dumb, expected, simple thing. Even if it adds another moving piece in the infra stack. Unless I need very strong data consistency guarantees, it's always better to use something like SQS, Redis queues, etc. where the understanding is that it is just a queue (or at least the API contract suggests simplicity), and then everything needs to work around it.
The fewer mechanistic responsibilities per core data store, the better in my experience.
It seems to me this can be boiled down to "using things without understanding how they work doesn't scale". Yes, vanilla listen/notify doesn't scale. But the OP actually figured out how to make it scale. So your engineers don't have to.
As the community builds, over time, distributed systems, it also understands which brick can do what, and it turns out that starting with less bricks and adding some when you actually need them makes for healthier systems.
And you can move redis to another server next later if needed to split the db and notification load.
> It is so rare that a new moving part beats other factors
Fair! I will restate that we were growing very fast, so that's probably an outlier environmental factor that I am potentially overly discounting. If that's not the case (likely for most startups), then maybe this is less of a problem.
The problem still needs to be solved (queue working/being up/being built around), but I am ok with the solution that might not be the best engineering solution but still delivers on the product requirements and extends the team's happiness (these systems need to be owned). These were not lazy or incompetent engineers.
We had a lot of success with LISTEN/NOTIFY when we paired it with a Rust graphql subscription broker. 10s of thousands of subscriptions, but only 3 or 4 LISTEN connections (one for each host). All changes would be pushed out to all hosts, who would each manage the actual user subscriptions and choose what to actually publish.
This worked super well. In general, moving from hundreds of Ruby or Node hosts to just a few Rust hosts just allows so many simplifications and things that "don't scale" to actually work quite well.
Once you start down the "durable workflows" path, you start seeing them everywhere.
My latest experiments are treating individual emails as durable workflows, where you, the people you're communicating with, agents and tools like GitHub or Attio all take turns in the flow.
https://housecat.com/blog/gmail-durable-workflows-sandbox-vm
1. What I find interesting is that the experiment seems to be using a DB server with 96 cores, 384 GB RAM (https://github.com/dbos-inc/dbos-postgres-benchmark/blob/mai...). This is very critical part of any such experiment, it should have been called out. The database is vertically scalable and that too has its limits
2. Who is making connection, and from where has its own impact on performance and overall latency
3. 60k may seem big number, however in real world the things which bring the systems down are the bursts of traffic, not the regular traffic.
Personally I would never start with such a big server unless I am a big business. Its > 100K cost for one production DB cluster if I include read replicas and cross region redundancy
What is the best way? Perhaps a tool that reads CDC and writes allocated event sequence numbers to another table would work ok, or would that have long latency?
And that CDC processor should then do the NOTIFY too.
Also on the consumer side in some usecases batching can drastically improve throughput. You don't even use LISTEN/NOTIFY then. Just run the consumer in a loop and each iteration process all new unprocessed messages, and store your last sequence number processed between iterations.
Postgres LISTEN/NOTIFY does not scale - https://news.ycombinator.com/item?id=44490510 - July 2025 (321 comments)
Since the correction apparently dates from May 8th, I think that a post from July 24th might want to acknowledge that the popular post asserting this feature doesn't (didn't?) scale was not made in bad faith or was even wrong about their claims at the time.
> As an aside, there’s been some online discussion of a Postgres patch (https://github.com/postgres/postgres/commit/282b1cde9dedf456...) related to this issue. This patch (to be released in Postgres 19) does not remove the global lock or fix the bottleneck we observed. Instead, it optimizes the narrower case where there are many notification channels and each listener is waiting only on a specific channel.
Update: Fixed in Postgres core
This commit has eliminated the bottleneck in the postgres core.
Credit to Joel Jacobson and the core postgres contributors for resolving this.
[1] https://www.recall.ai/blog/postgres-listen-notify-does-not-s...- Keeps messages O(1) so I can focus on scaling in the amount of notifications
- Tells whoever runs into this that,
- I didn't planned for arbitrarily large messages as they *might* otherwise grind performance to a halt.
- They *might* be misusing my notification systemBatching ensures you run at cpu & memory speeds and only pay significant latency for the flush - which usually linux kernel coalesces well if concurrent.
In the end, I decided to just go with the simplest solution possible. In my case it's just a barebones Go gRPC service that uses an in memory channel to send notifications between connected clients.
The reality is that this simple Go server will scale up to about 1000 simultaneously connected customers on about 2gb of RAM. I don't expect to have more than that many paying customers, and if I do I can always just throw a bigger VM at the problem.
Engineers love to over complicate things in the name of infinite scalability, when in reality you can save a lot of time and effort by just understanding the scope of the actual problem you're trying to solve. Fingers crossed that this will become an issue for me some day, but until then most of us just don't need to worry about it!
The real questions are:
- how many messages per second are you processing on that 2gb machine (and using how many cpus)?
- does your message processing involve transaction handling, including saving data ti disk durably?
No offense but it really seems you’re comparing apples and oranges, with your use case being much much simpler than the one described.
My point here is that it is important to match the tech stack to the challenge you're facing. When I started thinking about how to solve this problem my first reaction was to design an overly complicated distributed message queue using PG Notify, Redis, Kafka or something along those lines. The key takeaway here is that I realized that I probably wouldn't end up with more than 1000 customers, so I just needed to design a system that could comfortably handle that level of traffic without much effort. If, by some miracle, my business goes crazy viral, I know that my cloud provider can probably handle up to 200,000 customers by just updating a slider in my dashboard, which is way more business than I want anyway.
Engineers love to fantasize about Google levels of scale, but that's just not realistic for a lot of services.
What you wrote is essentially technical debt.
I would expect to have a little more tuning knobs available:
- explicitly unordered
- ordered per table, but not globally
The word "scale" does a lot of load-bearing, maybe it's just not a useful or productive word in practice.
Choose database queue technology https://news.ycombinator.com/item?id=37636841
It does seem interesting, and possibly welcome if there were a configuration option or even a way to set individual notifies as serialized or not.
The ceiling of LISTEN/NOTIFY is small enough that you need to pay attention, and I personally like to have at least an order of magnitude of slack left over even after my most pessimistic load numbers are accounted for, but it's still plenty for a lot of projects, and the integration with the rest of the DB, its availability, its not being another service you have to devops, it's definitely not something that should be simply dismissed out of hand as an option. Even the original 2K/s number they cite is a lot of messages for some systems that are more properly measured in seconds per message.