SQLStreams vs. Job Queues
Edit this pageWe owe this category a debt. River, pgmq, graphile-worker, Oban,
pg-boss, and Solid Queue spent years proving the thesis SQLStreams is built
on: Postgres is a great place for messages. SKIP LOCKED claiming,
producing inside the caller’s transaction, careful indexes on pending
work — these are the community’s hard-won patterns, and SQLStreams uses all
of them.
The difference is scope. A job queue answers one question: “run this work, reliably, soon.” SQLStreams answers that one and the questions that historically forced you to add Kafka and RabbitMQ next year: what happened? (retention), who else needs to know? (fan-out, routing), can we reprocess? (history a new consumer can read).
Feature by feature
Scored on shipped behavior; proposed capabilities are labeled, never checkmarked.
| pgmq | River | SQLStreams | |
|---|---|---|---|
At-least-once delivery, SKIP LOCKED | yes | yes | yes |
| Transactional produce | yes | yes | yes |
| Retries with backoff | basic visibility timeout | yes | yes |
| Dead-lettering | archive table | yes | yes, per consumer group |
| Retained history new consumers can read | no | no | yes — append-only log |
| Multiple independent consumer groups per message | no | no | yes |
| Routing bindings | no | no | yes — routing-key patterns |
| Per-key ordering | no | partial (unique/sequence opts) | per-key exclusivity or strict order (opt-in per message) |
| Model | SQS-style queue, SQL API | Go job runner | log + queue platform |
The structural difference is one design decision: job queues delete (or archive) on completion, so a message belongs to whoever claimed it first. That forecloses fan-out and replay permanently. SQLStreams retains the log and keeps per-group state beside it — a cursor for the happy path, delivery rows only for failures. The architecture page shows exactly how.
”Isn’t this just a job queue with extra steps?”
For your first use case — background work — SQLStreams behaves like a job queue, and you can use it as one: one stream, one consumer group, retries and dead-lettering included.
The bet is about your second and third use cases. The team that adopts a job queue almost always ends up, eighteen months later, also running:
- an events pipeline (Kafka/Kinesis) because product analytics and a new service both needed the order history the job queue had been deleting,
- a pub/sub layer (SNS/RabbitMQ) because three services needed the same notification and the job queue could only give it to one,
- an outbox + relay because the broker introduced the dual-write problem the Postgres queue never had.
Each addition is individually reasonable; the sum is the three-broker sprawl that Why SQLStreams opens with. SQLStreams’s pitch to job-queue users is simple: same start, different ceiling.
What about Hatchet, Inngest, Temporal?
Those are workflow engines — they orchestrate multi-step, long-running functions (often with a Postgres queue underneath, as in Hatchet’s case). Different layer of the stack: a workflow engine needs a durable message platform underneath it; a message platform doesn’t need a workflow engine. SQLStreams is the platform layer.