A six-person startup showed me their architecture diagram. Postgres for relational data, MongoDB for "flexible" documents, Redis for caching, Elasticsearch for search, RabbitMQ for jobs, and Pinecone for their new AI feature. Six data systems. Six sets of credentials, backups, upgrades, client libraries, failure modes and monitoring dashboards.
Their entire dataset was about 4GB.
Nobody made a bad decision, exactly. Each service was added by someone solving one problem well, following the advice that each tool should do one job. What nobody priced in was the compounding operational cost of six of them — and that Postgres could have done five of those jobs adequately, which for 4GB of data and six engineers was the correct trade.
The Argument Is Operational, Not Technical
To be clear: a dedicated search engine beats Postgres full-text search. A dedicated vector database beats pgvector at very large scale. Redis is faster than a Postgres table for caching. On any single axis, the specialist wins.
That is not the comparison that matters. The comparison is: is the specialist enough better, at your scale, to justify being a second thing you operate?
Because the second system is never just the second system. It is another backup and restore procedure you should test and probably do not. Another consistency boundary where data can disagree. Another dependency that can be down while the rest of your app is up. Another thing to upgrade. Another set of alerts. Another gap in whoever is on call at 3am.
For a small team, that cost dominates almost every performance difference you will actually experience.
What One Postgres Can Do
Documents. JSONB is a real document store with GIN indexes over it. If your data is mostly relational with a few schemaless fields, you do not need a document database — you need a column.
CREATE TABLE events (
id bigserial PRIMARY KEY,
tenant_id uuid NOT NULL,
type text NOT NULL,
payload jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX ON events USING gin (payload jsonb_path_ops);
SELECT * FROM events
WHERE tenant_id = $1 AND payload @> '{"status":"failed"}';
Full-text search. Good enough for searching your own content — a blog, a product catalogue, a help centre. It supports ranking, stemming and prefix matching, and it joins against your relational data in one query, which is something a separate search cluster genuinely cannot do without syncing.
Vector search. pgvector gives you embeddings with an HNSW index. Under a few million vectors it is entirely sufficient, and the advantage over a dedicated vector database is real: your embeddings and your permissions live in the same transaction, so filtering by tenant before the similarity search is a WHERE clause rather than a metadata-sync problem.
SELECT chunk_text, 1 - (embedding <=> $1) AS score
FROM doc_chunks
WHERE tenant_id = $2 -- filter first: correctness AND speed
ORDER BY embedding <=> $1
LIMIT 5;
Job queues. This one surprises people. SELECT ... FOR UPDATE SKIP LOCKED gives you a correct, concurrent work queue in about fifteen lines, and jobs are enqueued in the same transaction as the data change that caused them — so you cannot end up with a job for a row that was rolled back. That failure mode is common with an external broker and it is genuinely annoying to debug.
UPDATE jobs SET status = 'running', started_at = now()
WHERE id = (
SELECT id FROM jobs
WHERE status = 'pending' AND run_after <= now()
ORDER BY created_at
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING *;
Pub/sub and caching. LISTEN/NOTIFY handles light real-time fan-out. A table with a TTL column handles caching that does not need microsecond latency. Both are "good enough" rather than good, and good enough is often the right target.
When to Actually Add the Specialist
I am not arguing you should never add a second system. I am arguing you should add it for a named reason, measured, not preemptively. The reasons that hold up:
Redis when you need sub-millisecond reads at high rate, rate limiting at the edge, or ephemeral state you actively do not want durable. Usually the first genuine addition, and often the only one.
A search engine when search is your product rather than a feature — faceting, complex relevance tuning, typo tolerance, tens of millions of documents.
A vector database past roughly 10 million vectors, or when recall and latency requirements are strict enough that you are tuning the index seriously.
A real broker — Kafka, SQS — when you need fan-out to multiple independent consumers, replay, or throughput a table cannot take.
A time-series database when you are writing high-frequency metrics forever and need automatic downsampling.
Note that every one of these is a scale or requirement threshold, not an architectural preference. If you cannot state which threshold you have crossed, you have not crossed one.
The Limits, Honestly
Postgres is not infinitely elastic and pretending otherwise is how people get burned.
Connections are processes. Each one costs real memory, and a few hundred will hurt. With serverless functions or many app instances you need PgBouncer or an equivalent pooler, and you need it before the traffic arrives rather than during the incident.
Writes do not scale horizontally. You get one primary. Read replicas help reads; heavy write throughput eventually needs partitioning or sharding, and that is real work.
Long transactions cause bloat. An open transaction blocks vacuum, dead tuples accumulate, and performance degrades in a way that is confusing the first time. Keep transactions short and never hold one open across an external API call.
Mixing workloads gets messy. Heavy analytical queries alongside transactional traffic will compete. That is what a read replica is for.
What I Would Actually Build
For a new product: Postgres, plus Redis when a caching or rate-limiting need genuinely appears. That is the stack. Add anything else in response to a measurement, not a diagram.
That startup consolidated to Postgres and Redis over about six weeks. The migration was tedious. What changed afterwards was not performance — performance was fine either way at 4GB. It was that deploys stopped involving six version matrices, the on-call runbook got short enough to read, and a new engineer could understand where data lived on their first day.
For six people, that was worth more than any benchmark.



