Just Use Postgres: Why the One-Database Argument Keeps Coming Back
There is an argument that resurfaces in developer forums every few months, usually as a four-word slogan: just use Postgres. Queues, caches, full-text search, and now vector search for your AI features — all of it, one database. The pitch keeps returning because two pressures keep growing: cloud bills that scale with every managed service you add, and AI stacks that sprout new components faster than teams can learn to operate them.
The interesting part is not whether the slogan is right. It is that both sides of this argument are making claims about completely different things, and most people arguing never notice.
How Postgres Became the Universal Candidate
Postgres was designed to be extended. Not retrofitted for it — designed for it, back when it was a Berkeley research project. Custom types, custom index access methods, and a first-class extension system are all core features, not bolted-on afterthoughts. When a new kind of data or a new search technique shows up, you can plug it in rather than migrate off.
Vector search is the cleanest demonstration. When embeddings became table stakes, the Postgres community did not build a new database. They shipped pgvector, an extension. That was the entire response. Need geospatial queries? PostGIS. Time series? TimescaleDB. Full-text search is already in core via tsvector and GIN indexes.
Queues are the party trick. SELECT ... FOR UPDATE SKIP LOCKED turns an ordinary table into a concurrency-safe job queue. That one clause is what makes it work — it lets a worker grab a row while skipping any rows another worker has already locked, so ten workers polling the same table never hand out the same job twice. Before SKIP LOCKED landed, building a queue on a relational database meant lock contention and creative hacks. After it, a durable queue is roughly forty lines of SQL.
Licensing helped too. Redis and Elasticsearch both changed their licenses and both triggered messy community forks — Valkey and OpenSearch exist because of it. Postgres uses the permissive PostgreSQL License and no single company owns it. When you are choosing infrastructure you expect to run for a decade, “nobody can pull the rug out” is worth more than it sounds.
The Argument Was Never About Performance
The most common rebuttal misses the point entirely: a Postgres queue cannot possibly beat Kafka. Correct. It cannot. Nobody serious claims otherwise.
The claim is about operational complexity. Picture a service running Postgres, Redis, Kafka, Elasticsearch, and a hosted vector database. That is five backup strategies, five monitoring setups, five incident runbooks, five upgrade cadences, five sets of credentials to rotate, and five CVE feeds to track. On a team of three, with five infrastructure components, nobody actually understands the whole system. They understand their corner and hope.
Then there is transactional consistency, which is the argument’s strongest card. Say you save an order and enqueue a payment job. If the database is Postgres and the queue is anything else, there is no atomicity between those two writes. The order commits and the enqueue fails. Or the job lands in the queue and the order rolls back, so a worker picks up a payment for an order that does not exist. The standard fix is the transactional outbox pattern, or sagas, or a change-data-capture pipeline — real engineering effort to paper over a gap that only exists because the queue lives somewhere else. Put the queue in a Postgres table and both writes go in the same transaction. The problem does not get solved. It stops existing.
And the bill. Managed services multiply rather than add. For an early-stage product with modest traffic, that fixed monthly floor hurts far more than the engineering time it supposedly saves.
The Counterargument: You Have Not Met Scale Yet
The other side is not naive, and it usually comes from people who have been paged at 3 a.m.
The sharpest objection is workload interference. Pile OLTP traffic, queue polling, full-text search, and vector similarity computation onto one Postgres instance and they fight each other. Vector search is CPU-hungry. Queue polling generates relentless lock and I/O churn. When those two spike together and your login query goes from 5ms to 200ms, the whole product feels broken. The real reason to run dedicated infrastructure is often not raw throughput — it is isolation. A separate box means a bad query in one workload cannot take down the others.
Postgres also has a structural weakness that queues expose directly. It uses MVCC: updating a row does not overwrite it, it writes a new version and leaves the old one as a dead tuple. A queue table with thousands of inserts and deletes per second generates dead tuples faster than autovacuum reclaims them. The table bloats, indexes bloat, and performance degrades on a curve that looks fine right up until it does not. Purpose-built queues were designed around this access pattern from day one and simply do not have the problem. Aggressive autovacuum_vacuum_scale_factor tuning on the queue table helps and is not optional.
Feature depth differs too. Postgres full-text search is genuinely usable, but it does not approach Elasticsearch’s analyzer ecosystem, highlighting, faceting, or relevance-tuning controls. The gap widens sharply for languages that need real morphological analysis — Korean, Japanese, Turkish, Finnish — where stemming is not a suffix-stripping problem. Same story for pgvector: excellent up to a point, but at hundreds of millions of vectors, dedicated vector databases win on index algorithms and sharding strategy, and it is not close.
The Real Dividing Line Is Not Size
The usual summary is “small services use Postgres, big services use dedicated tools.” That framing is lazy. The question is not how big you are. It is which axis breaks first.
Take queues. Hundreds of jobs per second? A Postgres queue is fine and will stay fine longer than you expect. But if you need to stream tens of thousands of events per second, with multiple consumer groups reading the same stream at independent offsets and replaying history, that is the exact problem Kafka was built for. You can simulate it in Postgres. The cost of the simulation will exceed the cost of running Kafka.
Caching follows the same logic. Postgres shared buffers plus the OS page cache already constitute a good cache, and a surprising number of “we need Redis” moments turn out to be missing-index moments. But a session store where microsecond latency matters, the data is disposable, and reads run into the hundreds of thousands per second — that is Redis. Serving it from Postgres burns connections and CPU on work the database gains nothing from doing.
Here is a test that cuts through most of these debates. Ask: if I remove this component, does something get slower, or does something stop working? Introduce the dedicated tool only for the second case. If the honest answer is “a bit slower,” Postgres almost always wins on total cost once you price in the operational overhead you were about to sign up for.
Where to Actually Start
The practical sequence: begin with one Postgres instance. Queue on a SKIP LOCKED table, search on GIN indexes, embeddings via pgvector. Then hide each of those behind an abstraction in your application code. Do not call the queue library directly from your handlers — define a JobQueue interface and let the Postgres implementation sit behind it.
When the queue genuinely becomes the bottleneck, you swap the implementation and leave everything else alone. That abstraction costs a few hours up front. Skipping it costs weeks later, and those weeks arrive during the exact period when the system is already on fire. Start on Postgres, but cut the escape hatch while you have time. That is the most useful conclusion this whole debate produces.
The other half is instrumentation. Deciding when to migrate by feel means you move too late or too early. Put queue wait time, dead tuple ratio, and p99 search latency on a dashboard from day one. Then the moment to switch shows up as a number on a graph instead of a hunch in a retro.
The Takeaway
“Just use Postgres” is not a claim that one database beats five specialized ones. It is a warning against buying complexity before you have confirmed you need it. Dedicated tools earn their keep when they meet the specific problem they were built for. Deployed before that problem arrives, they are pure operational overhead wearing the costume of good architecture.
So look at your stack. For every component running right now, can you name the thing that stops working without it? If any one of them makes you hesitate, that is where to start reading.
Comments
Loading comments...