Postgres as the default everything-store, and where it ends

The architecture diagram had nine boxes. Postgres for the application, Redis for caching, RabbitMQ for jobs, Elasticsearch for search, a vector database for the new AI feature, S3, a warehouse, an ETL tool to move between them, and a metrics store. The team was four engineers. Nobody was on call for nine systems, which meant in practice nobody was on call for six of them.

Every one of those choices was defensible individually. Together they were the reason the team shipped slowly: five of those nine were solving problems that the Postgres they were already running, backing up, monitoring and knew how to operate would have solved to a standard the product needed for years.

This isn't a "Postgres does everything" argument — it doesn't, and the interesting part of the discussion is exactly where each capability ends. It's an argument about ordering: start on the database you already operate, and add a specialist system when a specific, nameable constraint forces it — not in advance. Because every additional datastore costs you a backup story, a failure mode, a consistency boundary, an on-call rotation, and a synchronization pipeline nobody puts in the estimate.

Five things Postgres does well enough

NeedPostgres answerGood until
Job queueSELECT … FOR UPDATE SKIP LOCKED, or an extension like pgmqThousands of jobs/sec; fan-out and streaming replay semantics
Full-text searchtsvector + GIN, or a BM25 extensionRelevance tuning, faceting and analyzers become the product
Vector searchpgvector with HNSW; DiskANN-style options on managed platformsRoughly 10M vectors, or when filtering/hybrid becomes the product
Document storeJSONB + GIN, partial and expression indexesGenuinely schemaless multi-tenant data at very high write rates
CacheAn unlogged table, or just the buffer cache doing its jobSub-millisecond p99 at high concurrency; pub/sub semantics
Light analyticsMaterialized views, partitioning, columnar extensionsTens to low hundreds of GB scanned per query; concurrent BI load

The right-hand column is the point of the article. Each of those ceilings is real and each is much further away than the architecture-diagram instinct assumes.

Queues

SKIP LOCKED turned Postgres into a legitimate queue years ago: workers claim rows without blocking each other, and because the claim is a transaction, enqueueing work in the same transaction as the business change it depends on is trivial. That property is worth more than it sounds. The classic distributed bug — the row committed but the job never got queued, or vice versa — simply cannot happen, and the usual workaround (a transactional outbox) is a pattern you don't have to build.

Where it ends: sustained very high throughput, fan-out to many independent consumer groups, and replay semantics over a retained log. If you need a durable log that many consumers read at their own offsets, that's Kafka's job and Postgres will not fake it well.

Full-text search

Postgres full-text search is genuinely good: stemming, ranking, phrase queries, and a GIN index that stays fast on millions of documents. Combined with vectors in the same database you can do hybrid retrieval — combining a text rank with vector distance in one SQL statement — without a second system or a synchronization pipeline.

Where it ends: when search is the product. Custom analyzers per language, faceted navigation, typo tolerance, per-field boosting, relevance experimentation with an evaluation harness — that's a search platform, and Elasticsearch or OpenSearch exists for good reasons. The tell is a dedicated person tuning relevance.

Vectors

pgvector plus HNSW carries a real RAG workload to roughly ten million vectors, and the vectors live in the same transaction as the rows they describe — so a document update and its embedding update are atomic, and there's no index-sync pipeline to break. Managed platforms have added DiskANN-style indexes that push the ceiling further by trading RAM for SSD.

Where it ends: single-node scaling with no native vector sharding, and the point where filtered or hybrid search performance becomes the product rather than a feature. That's the graduation I've written about in pgvector as a vector database.

Documents and cache

JSONB with GIN indexing handles semi-structured data comfortably, and you keep joins and transactions with your relational data — usually the reason the document store was a bad fit in the first place. And a large fraction of caches I've reviewed exist because someone assumed a database read was expensive; a well-indexed Postgres query on a warm buffer cache is often faster than the network hop to Redis. Cache when you've measured, not on principle. Redis earns its place for genuine sub-millisecond p99 at high concurrency, pub/sub, and data structures Postgres doesn't have.

Where Postgres genuinely ends

Being honest about this is what makes the rest credible. Six real limits:

  • Single-node writes. One primary accepts writes. Read replicas scale reads, Citus shards, but the simple architecture has a write ceiling and reaching it means a real change.
  • Analytical scans. Row storage plus a per-query process model means large scans and heavy concurrent BI are the wrong shape. Columnar extensions help; a warehouse is still a warehouse.
  • Connection scaling. Each connection is a process, so thousands of them need a pooler in front. Not a flaw, but an operational fact that surprises people at exactly the wrong moment.
  • Long-running transactions and vacuum. MVCC means dead tuples need cleaning, and a long transaction blocks that cleanup. Table bloat and a stalled autovacuum are the classic Postgres incident, and they arrive when your workload changes rather than when your data grows.
  • Extension availability on managed services. Your cloud provider decides which extensions exist and which versions. This constrains real designs, and it's worth checking before you build on one.
  • Mixed workload interference. The one that bites when you follow this article's advice too far: a heavy analytical query, a vector index build and your transactional traffic sharing one instance means one workload can starve another. Mitigate with replicas, resource limits and scheduling — or by finally splitting the workload out.
graph TB
  APP["Application"] --> PG[("Postgres
primary")] PG --- Q["Queue
SKIP LOCKED / pgmq"] PG --- FTS["Full-text
tsvector + GIN"] PG --- VEC["Vectors
pgvector HNSW"] PG --- DOC["Documents
JSONB + GIN"] PG --> REP[("Read replica
reporting, heavy reads")] PG -->|"logical replication / CDC"| OUT["Specialist systems,
added one at a time"] Q -.->|"fan-out consumer groups,
retained log, very high rate"| KAFKA["Kafka"] FTS -.->|"relevance IS the product"| ES["Elasticsearch / OpenSearch"] VEC -.->|"> ~10M vectors, filtering
is the product"| VDB["Qdrant / Milvus"] PG -.->|"analytical scans,
concurrent BI"| DW["Warehouse / lakehouse"]

The solid lines are day one; the dotted lines are graduations, each triggered by a named constraint rather than by an architecture diagram. The important edge is the one in the middle: logical replication or CDC out of Postgres is what makes every graduation incremental — you add the specialist system as a consumer, run both, and cut over when it earns the traffic. Nothing here requires a rewrite, which is exactly why starting simple is not a trap.

The signals that you have crossed a line

Not vibes — specific, measurable triggers worth writing into your runbook so the decision is made on evidence rather than in a design review:

  • Queue → broker: queue-table bloat you can't vacuum fast enough, workers contending on claim, or a second and third consumer wanting independent replay of the same events.
  • FTS → search engine: someone's full-time job is relevance, or product wants faceting and typo tolerance that tsquery can't express.
  • pgvector → vector database: p95 climbing with vector count, index rebuilds no longer fitting a maintenance window, or filtered recall failing your evaluation set.
  • Postgres → warehouse: analytical queries interfering with transactional latency even on a replica, or scans measured in hundreds of gigabytes.
  • Single node → sharding: write throughput at the primary's ceiling with no more instance size to buy, or a dataset whose working set no longer fits memory.

Each of those is a metric you can put on a dashboard, which turns "should we adopt X?" from an opinion into a threshold.

The failure mode of this advice, taken too far. I've argued the case for consolidation, so here's the honest counterweight: one instance running your transactional workload, a queue, a vector index and analytical queries has a single failure domain and four workloads that can starve each other. A vector index rebuild consuming I/O will slow your checkout. A runaway analytical query will exhaust connections everyone else needs. And the operational subtlety that catches people: a long-running transaction anywhere blocks vacuum everywhere, so an analytical query held open for an hour causes bloat in your queue table, and the incident presents as a slow queue with no obvious cause. Mitigations are ordinary — read replicas for heavy reads, statement timeouts, separate connection pools per workload, monitoring on the oldest transaction — but they have to be deliberate. The version of this advice I'd actually defend is: one database technology, not necessarily one instance. Running three Postgres instances for three workloads still gives you one set of skills, one backup story and one operational model, while giving each workload a failure domain of its own. That's usually the sweet spot, and it's much cheaper than three different technologies.

Leaving without a rewrite

The objection to starting simple is that you'll pay for it later. You mostly won't, if you build two things in from the start.

Put each capability behind a narrow interface. A Queue with enqueue and claim. A Search with one method. A Retriever with search(vector, filter, k). When you graduate, you write a new implementation of a small interface instead of finding every call site. This is unglamorous and it's the whole trick.

Turn on logical replication early. It's how you feed a specialist system without a migration weekend: stand the new system up as a consumer, backfill, run both, compare, cut over. The same mechanism that powers CDC pipelines is what makes every graduation on that diagram incremental rather than a rewrite.

What to carry away

Default to the database you already operate, and count the real cost of each additional system — a backup story, a failure mode, a consistency boundary, an on-call rotation, and a sync pipeline. Postgres credibly covers queues (with transactional enqueue you'd otherwise build an outbox for), full-text search, vectors to roughly ten million, documents via JSONB, caching more often than people believe, and light analytics — each until a specific, nameable constraint arrives.

Know the real ends: single-node writes, analytical scans, connection scaling, vacuum interacting with long transactions, managed-service extension availability, and workload interference. Write the graduation triggers down as metrics so the decision is evidence rather than fashion.

Then make graduation cheap: narrow interfaces per capability and logical replication turned on early, so adding a specialist system is an incremental consumer rather than a migration. And take the sharper version of the advice — one database technology, not necessarily one instance — because separate instances give each workload its own failure domain while keeping one set of skills, one backup story and one operational model. That's the configuration that stays simple without becoming fragile.