Most “just use Postgres for everything” posts stop at the fun part. They show you that pgvector exists, that SKIP LOCKED gives you a queue, that tsvector does search, and then they wave their hands and tell you to collapse your stack. What they never tell you is where the floor gives out. Every one of these extensions has a specific number - a QPS, a row count, an ingest rate - past which it stops being clever and starts being a liability.

That number is the only interesting part. Knowing Postgres can do vector search is worthless. Knowing it does vector search fine up to roughly 20 to 50 million vectors on a single well-provisioned box, and falls apart past that, is the thing that actually changes your architecture decision.

So this is not another list of what Postgres can do. It is a map of what each extension replaces and where the ceiling sits, so you can consolidate on purpose and split out on purpose instead of discovering the limit in production at 2 AM.

The stack you are probably running

Here is the default 2026 startup backend, assembled one panic at a time:

                  +-------------+
   users, orders  |  Postgres   |
        --------->|  (OLTP)     |
                  +-------------+
   embeddings     +-------------+
        --------->|  Pinecone   |   $300-500/mo
                  +-------------+
   jobs           +-------------+
        --------->|  SQS / Rabbit|
                  +-------------+
   search         +-------------+
        --------->| Elasticsearch|  3-node cluster
                  +-------------+
   metrics        +-------------+
        --------->|  InfluxDB   |
                  +-------------+
   dashboards     +-------------+
        --------->|  ClickHouse |
                  +-------------+

Six systems. Six failure modes. Six backup strategies. Six things to keep in sync with your primary data, which means six chances for the copy to drift from the source of truth. And critically, five of these hold data that is derived from or joined against the data in Postgres, which means every read that spans two systems is a network hop and a consistency gamble.

The consolidation argument is not “Postgres is faster.” It usually is not faster than a purpose-built system at that system’s one job. The argument is that a transaction that writes your order and its embedding and enqueues the fulfillment job in one COMMIT is a category of correctness you cannot buy at any price once those live in separate systems.

Let me go through them one at a time.

Vectors: pgvector replaces Pinecone / Weaviate

This is the one everybody reaches for, and it is the one most likely to be genuinely sufficient.

pgvector stores embeddings as a native column type and gives you HNSW and IVFFlat indexes for approximate nearest-neighbor search. The whole appeal is that your embedding lives in the same row as the record it describes:

CREATE EXTENSION vector;

ALTER TABLE documents ADD COLUMN embedding vector(1536);

CREATE INDEX ON documents
  USING hnsw (embedding vector_cosine_ops)
  WITH (m = 16, ef_construction = 64);

-- retrieval joined against real application data in one query
SELECT d.id, d.title, d.tenant_id,
       d.embedding <=> $1 AS distance
FROM documents d
WHERE d.tenant_id = $2          -- a filter Pinecone makes you fight for
  AND d.archived = false
ORDER BY d.embedding <=> $1
LIMIT 10;

That WHERE d.tenant_id = $2 is the quiet superpower. In a dedicated vector database, metadata filtering is a bolt-on that often degrades recall or forces you to over-fetch and filter in your app. In Postgres it is just a predicate the planner already knows how to use.

Where it breaks. HNSW index build and memory are the ceiling, not query latency. The index wants to live in RAM. Somewhere between 20 and 50 million vectors at 1536 dimensions - call it 100 to 300 GB of index - you are provisioning a machine around the index rather than around your workload, index builds take hours, and ef_search tuning becomes a daily chore. Past that, or once you need billions of vectors with heavy filtered recall guarantees, a dedicated store earns its bill.

Do not split until: you have measured recall at your real filter selectivity, tried halfvec (2-byte floats, halves index size at negligible quality loss for most models), and confirmed the index no longer fits in RAM. Most teams never get close.

Queues: pgmq and SKIP LOCKED replace SQS / RabbitMQ

The naive version everyone writes first is a jobs table with a status column and a worker that runs SELECT ... WHERE status = 'pending' LIMIT 1 then UPDATE. This is broken. Two workers read the same row before either updates it, and you process the job twice. People then reach for a real broker to escape the race.

You do not need to. FOR UPDATE SKIP LOCKED is the whole answer:

-- one worker's atomic claim, safe under concurrency
UPDATE jobs
SET status = 'processing', locked_at = now()
WHERE id = (
    SELECT id FROM jobs
    WHERE status = 'pending' AND run_after <= now()
    ORDER BY id
    FOR UPDATE SKIP LOCKED     -- skip rows another worker already grabbed
    LIMIT 1
)
RETURNING id, payload;

SKIP LOCKED tells Postgres to walk past rows another transaction has locked instead of blocking on them, so N workers pull N distinct jobs with zero coordination and zero double-processing. If you want batteries included - visibility timeouts, archival, a partitioned queue table - pgmq (from the Tembo folks) wraps exactly this pattern with an SQS-like API and keeps everything transactional with your business writes. That last part matters: you can enqueue a job in the same transaction that creates the order, so a rolled-back order never leaves a ghost job behind.

Where it breaks. Throughput and the vacuum tax. A single Postgres instance comfortably handles thousands of jobs per second this way, into the low tens of thousands with tuning. The real killer is dead tuples: a high-churn queue table generates enormous vacuum pressure, and if autovacuum falls behind, your queue scans slow to a crawl. Partition the queue, archive aggressively, and watch bloat.

Need Stay on Postgres Split to a broker
< ~10k jobs/sec Yes, use pgmq No
Transactional enqueue with business data Yes, this is the point No
Fan-out to many consumer groups, replay No Kafka
Millions of msgs/sec, ordered partitions No Kafka / Pulsar

Do not split until: you are past ~10k jobs/sec sustained, or you genuinely need durable replayable log semantics (multiple independent consumer groups reading the same stream). Delivery-once task queues almost never need that.

Full-text search: tsvector and ParadeDB replace Elasticsearch

Built-in tsvector search is older than most people running it. It does stemming, ranking, and prefix matching, and with a GIN index it is genuinely fast:

ALTER TABLE articles ADD COLUMN search tsvector
  GENERATED ALWAYS AS (
    setweight(to_tsvector('english', coalesce(title,'')), 'A') ||
    setweight(to_tsvector('english', coalesce(body,'')),  'B')
  ) STORED;

CREATE INDEX articles_search_idx ON articles USING gin (search);

SELECT id, title, ts_rank(search, query) AS rank
FROM articles, websearch_to_tsquery('english', $1) query
WHERE search @@ query
ORDER BY rank DESC
LIMIT 20;

The GENERATED ... STORED column means the search vector is maintained automatically on every write, in the same transaction, so it can never drift from the source row. That alone kills the single worst Elasticsearch failure mode: the sync pipeline silently falling behind and serving stale results.

For 2026 the real story is ParadeDB (the pg_search extension), which embeds a Tantivy-based BM25 index directly in Postgres. That closes most of the actual quality gap - real BM25 relevance scoring, fast faceting, fuzzy matching - without a separate cluster or a Logstash pipeline.

Where it breaks. Built-in tsvector is fine for keyword filtering and “good enough” relevance up to tens of millions of documents, but its ranking is crude (ts_rank is not BM25) and it has no native fuzzy or typo tolerance. pg_search pushes that ceiling much higher. You split to Elasticsearch/OpenSearch when you need heavy aggregations across billions of documents, cross-cluster geo-distribution of the index, or the deep analytics/log-search feature set that is Elastic’s actual home turf.

Do not split until: plain tsvector relevance is provably not good enough (test it, do not assume), and pg_search also falls short. Most product search - the search box on your app - never leaves Postgres.

Time-series: TimescaleDB replaces InfluxDB

I have covered the time-series side in depth before, so the short version: TimescaleDB turns a table into a hypertable that is transparently partitioned into time-based chunks, adds columnar compression, and precomputes rollups with continuous aggregates.

SELECT create_hypertable('metrics', 'time', chunk_time_interval => INTERVAL '1 day');

-- precomputed rollup, refreshed incrementally, queried like a table
CREATE MATERIALIZED VIEW metrics_hourly
WITH (timescaledb.continuous) AS
SELECT time_bucket('1 hour', time) AS bucket,
       sensor_id,
       avg(value), max(value)
FROM metrics
GROUP BY bucket, sensor_id;

Compression alone routinely cuts storage 90%+ on real metrics, and continuous aggregates mean your dashboards read a small precomputed table instead of scanning billions of raw rows.

Where it breaks. Ingest rate. A single node handles hundreds of thousands of inserts per second with batching, which covers the overwhelming majority of application and product metrics. You outgrow it when you are at true observability scale - millions of samples per second, Prometheus-fleet territory - or need the specific columnar-at-any-cost query profile that a purpose-built ingestion engine gives. At that point you are running a metrics platform, not a feature of your app.

Do not split until: batched ingest is saturating a node and continuous aggregates are not keeping your dashboards fast. That is a real scale, and you will have plenty of warning.

Analytics: columnar extensions replace ClickHouse / a warehouse

This is the shakiest replacement of the five, so be honest with yourself here. Postgres is row-oriented, which is exactly wrong for SELECT avg(x) FROM billions_of_rows. The 2026 options that help:

  • pg_duckdb embeds DuckDB inside Postgres to run vectorized, columnar analytical queries over your Postgres tables and Parquet files in object storage.
  • Hydra / columnar storage gives you column-oriented tables in the same instance for scan-heavy aggregates.

For dashboards over the tens-to-low-hundreds of millions of rows, and for querying Parquet in S3 without standing up a warehouse, this is legitimately good and saves you an entire ETL pipeline plus a Snowflake bill.

Where it breaks. This is the extension you should be most eager to split out. Once analytics means concurrent complex queries over billions of rows, or you have a dedicated analytics team wanting to run heavy jobs without touching production OLTP latency, keep it off your primary. Even here, the clean move is often a read replica plus DuckDB rather than a full warehouse - you get isolation without a new vendor.

Do not split until: analytical queries are stealing CPU and cache from your transactional workload, or query volume outgrows what a replica absorbs. Isolation, not raw capability, is the trigger.

The whole map, on one page

Dedicated service Postgres replacement Comfortable ceiling Hard split trigger
Pinecone / Weaviate pgvector (+ halfvec) ~20-50M vectors, index fits RAM Billions of vectors, filtered recall SLAs
SQS / RabbitMQ pgmq / SKIP LOCKED ~10k jobs/sec Replayable log, many consumer groups
Elasticsearch tsvector / pg_search 10s of millions of docs Petabyte log analytics, cross-region index
InfluxDB TimescaleDB 100k-1M+ inserts/sec Prometheus-fleet observability scale
ClickHouse / Snowflake pg_duckdb / columnar 10s-100s of millions of rows Billions of rows, isolated analytics team

The pattern across every row is the same: the ceiling is set by volume and isolation, never by capability. Postgres can do the thing. The question is whether doing the thing next to your OLTP workload starves it.

What actually works, and what does not

What works. Vectors, queues, and full-text search inside Postgres are the real wins and almost nobody outgrows them. If you are a team under, say, 30 engineers, collapsing Pinecone, your job broker, and Elasticsearch back into Postgres is very likely a net reduction in incidents. The transactional guarantee - write the row and its embedding and its job atomically - is a correctness property you cannot get any other way, and it is worth more than the throughput you give up.

What does not. Analytics is the weak link. Row storage fights you, and the honest move at scale is a replica or a warehouse. Do not let “Postgres can do analytics” talk you into running heavy aggregate jobs against your production primary - that is how you turn a search feature into an OLTP outage.

The failure mode to avoid. The opposite mistake is just as expensive: consolidating everything, hitting a ceiling you never measured, and doing an emergency migration under load. Consolidation only pays off if you know your numbers. Before you collapse a service, write down the ceiling from the table above, add monitoring for the metric that defines it (index-size-vs-RAM, jobs/sec, ingest rate, replica lag), and set an alert at 70% of the limit.

What to actually do. Start every new service inside Postgres. Add the extension, not the vendor. Instrument the one number that defines its ceiling. Split out exactly when that number says to and not a day earlier - which for most teams, on most of these, is never. Two databases you understand deeply will always beat six you operate half-asleep.