Hacker News
PgQue: Zero-Bloat Postgres Queue
killingtime74
|next
[-]
It's Kafka like one event stream and multiple independent worker cursors.
It's more SNS than SQS or Kafka than Rabbitmq/Nats
saberd
|next
|previous
[-]
Then in the latency tradeof section it says end to end latency is between 1-2 seconds.
Is this under heavy load or always? How does this compare to pgmq end to end latency?
samokhvalov
|root
|parent
|next
[-]
I didn't understand nuances in the beginning myself
We have 3 kinds of latencies when dealing with event messages:
1. producer latency – how long does it take to insert an event message?
2. subscriber latency – how long does it take to get a message? (or a batch of all new messages, like in this case)
3. end-to-end event delivery time – how long does it take for a message to go from producer to consumer?
In case of PgQ/PgQue, the 3rd one is limited by "tick" frequency – by default, it's once per second (I'm thinking how to simplify more frequent configs, pg_cron is limited by 1/s).
While 1 and 2 are both sub-ms for PgQue. Consumers just don't see fresh messages until tick happens. Meanwhile, consuming queries is fast.
Hope this helps. Thanks for the question. Will this to README.
mind-blight
|next
|previous
[-]
Scaling the workers sometimes exacerbates the problem because you run into connection limits or polling hammering the DB.
I love the idea of pg as a queue, but I'm a more skeptical of it after dealing with it in production
odie5533
|next
|previous
[-]
cout
|next
|previous
[-]
andrewstuart
|next
|previous
[-]
Any database that supports SKIP LOCKED is fine including MySQL, MSSQL, Oracle etc.
Even SQLite makes a fine queue not via skip locked but because writes are atomic.
halfcat
|next
|previous
[-]
1. SKIP LOCKED family
2. Partition-based + DROP old partitions (no VACUUM required)
3. TRUNCATE family (PgQue’s approach)
And the benefit of PgQue is the failure mode, when a worker gets stuck:
- Table grows indefinitely, instead of
- VACUUM-starved death spiral
And a table growing is easier to reason about operationally?
samokhvalov
|root
|parent
[-]
in all three approaches, if the consumer falls behind, events accumulate
The real distinction is cost per event under MVCC pressure. Under held xmin (idle-in-transaction, long-running writer, lagging logical slot, physical standby with hot_standby_feedback=on):
1. SKIP LOCKED systems: every DELETE or UPDATE creates a dead tuple that autovacuum can't reclaim (xmin is frozen). Indexes bloat. Each subsequent FOR UPDATE SKIP LOCKED scans don't help.
2. Partition + DROP (some SKIP LOCKED systems already support it, e.g. PGMQ): old partitions drop cleanly, but the active partition is still DELETE-based and accumulates dead tuples — same pathology within the active window, just bounded by retention. Another thing is that DROPping and attaching/detaching partitions is more painful than working with a few existing ones and using TRUNCATE.
3. PgQue / PgQ: active event table is INSERT-only. Each consumer remembers its own pointer (ID of last event processed) independently. CPU stays flat under xmin pressure.
I posted a few more benchmark charts on my LinkedIn and Twitter, and plan to post an article explaining all this with examples. Among them was a demo where 30-min-held-xmin bench at 2000 ev/s: PgQue sustains full producer rate at ~14% CPU; SKIP LOCKED queues pinned at 55-87% CPU with throughput dropping 20-80% and what's even worse, after xmin horizon gets unblocked, not all of them recovered / caught up consuming withing next 30 min.