A team I know spent six weeks operationalising Kafka for a workload doing two hundred messages per second. Three nodes. ZooKeeper. Schema registry. Two engineers who had not used it before, becoming the on-call for it. They were proud. They had a job-queue product.
Postgres did the same job in forty lines of code. SELECT FOR UPDATE SKIP LOCKED is in every recent version. Throughput on a t3.medium is in the low thousands of messages per second without breaking a sweat. Delivery semantics are exactly-once because you’re inside a transaction. The thing you’ve already got running and paying for can do this.
When Postgres is enough
Three conditions, in roughly decreasing order of generosity:
- Throughput under a thousand events per second. Most internal queues live here. The “we might scale” argument fails on the math — a tenfold growth still fits.
- Producers and consumers are on the same database. If the consumer needs to update other tables when it processes, doing it in one transaction is the killer feature you give up by moving the queue out.
- You can tolerate a Postgres outage taking the queue down too. For most internal workloads, your app already can’t survive a Postgres outage. The queue going with it doesn’t change your availability story.
When you actually need Kafka
Three real reasons, in order of how rarely they apply:
- Multi-consumer fan-out with replay. You want N independent consumers each reading the same events, with the ability to rewind. Postgres can simulate this badly; Kafka does it natively.
- Sustained throughput over ten thousand events per second with low producer-side latency requirements.
- You already have a team running Kafka. This is the most honest reason and the one nobody says out loud.
If none of those apply, you do not need Kafka. You need a jobs table, a small daemon that polls it, and a Sunday afternoon.
The point isn’t that Kafka is bad. It’s that the boring option is usually load-bearing in ways the exciting option won’t be for another two years. By then the team that picked boring shipped twice.
The strongest case against thisKafka buys organizational clarity, not just throughput
The Postgres-default argument may understate the value of an explicit event log when multiple teams need replay, ownership boundaries, and independent consumers.
- What would make me wrong
- A low-throughput workload still needs durable replay, independent consumers, or cross-team ownership boundaries that Postgres queue tables make awkward or fragile.
- Last reviewed
The strongest case against “Postgres is enough” is that queues are often organizational interfaces, not just message buffers. Kafka gives teams a durable log, named topics, consumer groups, retention policy, replay, and a shared language for event ownership. Those properties can matter before throughput does.
Postgres queue tables also tend to start simple and accrete product assumptions: one consumer, one retry policy, one schema shape, one transaction boundary. The moment another team wants replay, analytics, or independent consumption, the “forty lines” become a homegrown event platform. Kafka is heavy, but sometimes the weight is the feature.
The essay is strongest for single-team internal work. It is weakest when the queue is a contract between teams. In that case, picking Kafka early can be less about scale theater and more about avoiding a hidden coupling that Postgres makes too easy.