Skip to content

The Postgres Queue Ceiling Just Moved

DBOS reports 30,000 jobs per second — the real lesson is which optimization bought each order of magnitude.

Emeka Okafor
Emeka Okafor
Security Editor · Jul 30, 2026 · 5 min read
The Postgres Queue Ceiling Just Moved

"Postgres doesn't scale as a queue" has been repeated so often it's practically folklore. The standard advice: fine for a side project, but real workloads need RabbitMQ, Redis and BullMQ, or SQS. DBOS — the durable-execution company co-founded by Postgres creator Michael Stonebraker's academic lineage — just published numbers that push hard against that folklore: 30,000 workflow executions per second through Postgres-backed queues, roughly 80 billion a month, after three rounds of optimization. The post hit the Hacker News front page this week, and it deserves a closer read than the headline number — because the interesting part isn't the 30K. It's which fix bought each order of magnitude.

The bottleneck ladder

DBOS's account is effectively a ladder of ceilings. A naive dequeue query — SELECT the oldest enqueued rows, claim them — collapses around 100 jobs per second because every worker fights over the same rows. FOR UPDATE SKIP LOCKED fixes that: each worker locks a batch and everyone else skips past locked rows to the next available work.

The next ceiling appeared near 1,000 dequeues per second, and it's the one worth internalizing. DBOS ran dequeue transactions at REPEATABLE READ because global concurrency limits ("at most N jobs running across the whole fleet") need a consistent view of queue state. Under high concurrency, Postgres's snapshot isolation answers contention with serialization failures, and workers ended up spending more time retrying aborted transactions than doing work. The fix wasn't clever SQL — it was a product realization: almost nobody running at scale actually uses global limits. Per-worker limits ("10 concurrent jobs per process") need no cross-worker coordination at all. So the isolation level became conditional: READ COMMITTED by default, REPEATABLE READ only for queues that explicitly ask for global flow control.

That's the general lesson hiding in a vendor post: coordination is a feature you should pay for only where you've asked for it. A global concurrency cap is a distributed-systems promise, and it costs you an isolation level. Most teams set those limits by habit, not need.

The third ceiling, around 8,000 jobs per second, was CPU — burned by the dequeue query's sort step and by autovacuum grinding through index churn. The fix was making every hot index partial: the dequeue index covers only rows WHERE status = 'ENQUEUED', sorted by priority and creation time, so it contains nothing but waiting jobs and the query needs no sort. Observability indexes got the same treatment — the parent-workflow index only covers rows that have a parent. Completed jobs simply stop existing as far as the hot path is concerned.

Most of this is rediscovery — and that's the point

To their credit, DBOS calls SKIP LOCKED "one of those old Postgres tricks that keeps getting rediscovered." It shipped in Postgres 9.5 back in 2016, and it's the beating heart of every serious Postgres-native queue since: Solid Queue, which Rails 8 now ships as its default job backend; Oban in Elixir; River in Go; pg-boss in Node. If you adopt any of these, lesson one is already done for you.

The vacuum lesson is the historically important one, because MVCC bloat — not lock contention — is why Postgres queues actually fell over in the wild for a decade. A queue is a pathological workload for Postgres's storage model: every job is a row that gets updated several times and then becomes garbage, so a busy queue manufactures dead tuples at line rate and dares autovacuum to keep up. Partial indexes don't repeal MVCC, but they shrink the blast radius dramatically — the index that matters stays small and cheap to maintain no matter how many millions of completed rows are awaiting cleanup.

It's also worth noting what DBOS's design quietly avoids. Their workers poll; they don't use LISTEN/NOTIFY for wakeups. That looked old-fashioned until recall.ai's widely read postmortem last year showed that NOTIFY takes a global commit-serializing lock under heavy write load — the "elegant" push-based design is the one that melts down. Boring polling against a well-indexed table turns out to be the scalable choice.

Read the number with squinted eyes

Now the caveats, because there are real ones. This is a vendor benchmark with no methodology attached: no Postgres version, no instance size, no disclosure of how "30K per second across thousands of servers" maps onto database instances — one big primary or many shards changes the story materially. DBOS sells Postgres-backed durable execution; its business depends on this conclusion. Nothing in the post smells wrong — every technique is verifiable, standard-issue Postgres engineering — but treat 30K as "achievable with effort and unspecified hardware," not as a number your RDS instance will hit.

The honest framing is this: the ceiling for a tuned Postgres queue is now comfortably in the thousands of jobs per second, and the overwhelming majority of teams never get within an order of magnitude of that. If you're under a few hundred jobs per second — which is most companies, most of the time — the operational argument is lopsided. A queue in Postgres lives inside your existing backups, your existing transactions (enqueue a job atomically with the commit that creates the work — no outbox pattern, no dual-write bugs), your existing monitoring, and one psql prompt when things go sideways at 2 a.m.

Where does a dedicated broker still win? Fan-out pub/sub to many consumers, replayable streams, and retention-heavy event logs are Kafka-shaped problems; don't force them into a jobs table. Extreme burst absorption with huge backlogs will still bloat Postgres in ways SQS shrugs off. And if your queue shares an instance with a latency-sensitive OLTP workload, autovacuum pressure is a real noisy-neighbor risk — the pragmatic middle ground is a separate Postgres instance for the queue, which still costs less operational surface than a broker plus its client libraries and its own failure modes.

The practical takeaway isn't "migrate to DBOS." It's that the burden of proof has flipped. In 2016, choosing Postgres as your queue required a justification. In 2026 — with SKIP LOCKED battle-proven, Rails shipping it as the default, and existence proofs in the tens of thousands of jobs per second — it's the dedicated broker that needs one. Check what your queue library's dequeue index looks like, find out whether your concurrency limits are global or per-worker, and you may discover the scaling problem you were architecting around doesn't exist.

Sources & further reading

  1. Making Postgres Queues Scale — dbos.dev
  2. Making Postgres queues scale — news.ycombinator.com
  3. Postgres LISTEN/NOTIFY does not scale — recall.ai
  4. Solid Queue: Database-backed Active Job backend — github.com
  5. SKIP LOCKED: One of my favorite PostgreSQL 9.5 features — cybertec-postgresql.com
Emeka Okafor
Written by
Emeka Okafor · Security Editor

Emeka has spent over a decade tracking threat actors, vulnerability disclosures, and the evolving landscape of application security, bringing a sharp continent-spanning perspective to his reporting. He's known for translating dense CVE advisories into clear, actionable context that developers and security teams alike actually read.

Discussion 0

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading