Skip to content
Dev Tools Article

The Postgres Swiss Army Knife Is Not a Free Lunch

Before consolidating your entire stack onto a single database, understand the operational limits of the Postgres is enough philosophy.

Rachel Goldstein
Rachel Goldstein
Dev Tools Editor · Jul 6, 2026 · 6 min read
The Postgres Swiss Army Knife Is Not a Free Lunch

The modern developer stack has a complexity problem. For a typical early stage application, the default architecture often looks like a distributed systems textbook. You need a cache, so you spin up Redis. You need background workers, so you add Sidekiq. You need search, so you bolt on Elasticsearch. Before long, a simple CRUD app is talking to half a dozen data stores, each with its own deployment pipeline, backup strategy, and unique way of breaking at three in the morning.

This architectural bloat has triggered a healthy counter-movement. The core thesis of the "Postgres is enough" philosophy is that PostgreSQL can handle almost all of these workloads. Instead of managing a sprawling portfolio of specialized databases, you can run your cache, your queues, your document store, and your vector search inside a single Postgres instance.

It is an incredibly compelling pitch. But while Postgres is remarkably versatile, treating it as a universal runtime is not a free lunch. To use it successfully without painting yourself into an architectural corner, you need to understand exactly where the technical boundaries lie.

How Postgres Masquerades as Other Systems

Postgres can replace specialized infrastructure because its extension ecosystem and core features have evolved far beyond simple relational storage.

The Job Queue: SKIP LOCKED

Historically, using a relational database as a queue was an anti-pattern because locking rows during worker selection caused massive contention. Postgres solved this with the introduction of SELECT ... FOR UPDATE SKIP LOCKED. This allows multiple workers to query the same table, lock the rows they are processing, and skip over already locked rows without blocking.

UPDATE job_queue
SET status = 'processing', locked_at = NOW()
WHERE id = (
  SELECT id
  FROM job_queue
  WHERE status = 'queued'
  ORDER BY priority DESC, created_at ASC
  LIMIT 1
  FOR UPDATE SKIP LOCKED
)
RETURNING *;

This simple query forms the basis of modern Postgres-backed queue libraries like pgmq. It eliminates the need for Redis or RabbitMQ for most standard background processing workloads.

The Document Store: JSONB and GIN Indexes

If you need a flexible schema, you do not need to default to MongoDB. Postgres has native support for JSONB, which stores JSON data in a decomposed binary format. To make queries fast, you can back these columns with Generalized Inverted Index (GIN) indexes.

CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  profile JSONB
);

CREATE INDEX idx_users_profile ON users USING gin (profile);

This setup allows you to query deep nested keys with index support, giving you the schema flexibility of a document database alongside ACID compliance.

Vector Search: pgvector

Instead of paying for a specialized vector database like Pinecone, the pgvector extension allows you to store and index vector embeddings directly alongside your relational data. With extensions like pgvectorscale, Postgres can handle high-dimensional vector searches using specialized index types, keeping your AI-driven features in the same transactional boundary as your user data.

The Operational Reality: When the Knife Blunts

Consolidating these workloads onto Postgres simplifies your deployment topology, but it concentrates all your operational risk into a single point of failure. When you run everything on one database, you must confront several hard technical realities.

Resource Contention

In a single-server setup, your application runtime and your database often share the same virtual private server (VPS). As traffic grows, heavy background workers or analytical queries can easily starve the database of CPU, RAM, or disk I/O.

Even if you separate the application layer from the database server, a single Postgres instance hosting multiple workloads will experience internal resource contention. Postgres relies heavily on its shared buffer pool (configured via the -B option or shared_buffers parameter) to cache table data in memory. If your application is constantly churning through large JSON documents, running vector similarity searches, and polling a job queue, these workloads will fight for space in the shared buffers, constantly evicting each other's hot data and forcing slow disk reads.

The Process-Per-Connection Bottleneck

Unlike thread-based databases, Postgres spawns a separate operating system process for every single client connection. This model is highly stable, but it is memory-intensive.

If you are using Postgres for caching, queuing, and standard OLTP queries, your application servers will maintain a high number of concurrent connections. Without an external connection pooler like PgBouncer, the overhead of managing hundreds of active processes will degrade database performance. Redis, by contrast, uses a single-threaded event loop that handles tens of thousands of concurrent connections with minimal memory overhead.

The Danger of Performance Shortcuts

When trying to make Postgres match the write speeds of in-memory caches or queues, developers are sometimes tempted to use dangerous configuration shortcuts. For example, the postgres server application allows you to disable write-ahead log (WAL) synchronization using the -F flag (which disables fsync calls). While this dramatically improves write performance, a system crash or power failure will almost certainly result in data corruption.

Similarly, while UNLOGGED tables bypass the WAL and are excellent for transient cache data, they are completely wiped during an unclean shutdown. If you are relying on them for critical application state, you are playing with fire.

Navigating the Postgres "Cluster"

To mitigate these issues, you must understand how Postgres organizes data. In Postgres terminology, a "database cluster" is not a group of physical servers. It is a single instance of the postgres server application running on a single port (typically 5432) and managing a single data directory on disk.

Within that single cluster, you can host multiple independent databases. This is a common pattern for microservices, allowing each service to have its own database schema while sharing the same underlying database process.

flowchart TD
    App[Application Server]
    subgraph PG_Cluster [Postgres Instance / Port 5432]
        DB1[(OLTP Database)]
        DB2[(Queue Database)]
        DB3[(Cache Database)]
    end
    App --> DB1
    App --> DB2
    App --> DB3

While this keeps your infrastructure footprint small, it does not isolate resources. A runaway query on your queue database will still saturate the CPU of the entire instance, degrading performance for your primary OLTP database.

If you need true resource isolation without introducing entirely different database engines, the solution is to run multiple physical Postgres instances on different ports (such as 5432 and 5433) with separate data directories, or split them onto separate virtual servers entirely. This preserves the operational benefit of using a single database engine (one backup tool, one SQL dialect) while preventing a heavy background queue from taking down your user-facing application.

The Architectural Tipping Points

Postgres is absolutely the right place to start for 99% of projects. It defers the operational tax of specialized databases until you actually have the scale to justify them. However, you should actively plan to migrate specific workloads when you hit these concrete thresholds:

  • For Queues: If your queue throughput exceeds thousands of jobs per second, or if the constant deadlocks and table bloat from rapid INSERT and DELETE operations on your queue tables begin to degrade your primary OLTP transaction performance.
  • For Search: If you need complex typo tolerance, stemming, and relevance scoring across millions of documents. While extensions like ParadeDB bring Elasticsearch-like capabilities to Postgres, a dedicated search engine is still superior for heavy, user-facing search workloads.
  • For Time-Series: If you are ingestion-heavy and your tables are growing by gigabytes per day. At this point, you should either adopt a specialized extension like TimescaleDB or move to a dedicated time-series database to handle automatic partitioning and compression.

Start with Postgres, keep your architecture boring, and only split your data stores when resource monitoring, not hype, dictates the transition.

Sources & further reading

  1. Do you need separate systems when you already have Postgres? — postgresisenough.dev
  2. sql - If I'm using PostgreSQL, do I need a server too? Like AWS RDS? - Stack Overflow — stackoverflow.com
  3. When To Separate Database And Application Servers For MySQL And PostgreSQL | DCHost.com Blog — dchost.com
  4. PostgreSQL: Documentation: 18: postgres — postgresql.org
  5. postgresql database cluster vs one server with many databases - Database Administrators Stack Exchange — dba.stackexchange.com
Rachel Goldstein
Written by
Rachel Goldstein · Dev Tools Editor

Rachel has been embedded in the developer tooling ecosystem for nearly eight years, covering everything from IDE wars and package-manager drama to the quiet rise of AI-assisted coding. She has a soft spot for open-source maintainers and an unhealthy number of terminal emulators installed on a single laptop.

Discussion 2

Join the discussion

Sign in or create an account to comment and vote.

Dana Reyes @hypewatch_dana · 6 hours ago

i'm all for simplifying the stack, but does anyone have actual numbers on how postgres performs when handling search and caching workloads compared to dedicated solutions like elasticsearch and redis? okay but does it actually hold up in production?

Jen Okafor @rustacean_jen · 8 hours ago

i love how this article highlights the complexity of modern dev stacks, it reminds me of how rust's ownership model helps simplify system design by enforcing memory safety and performance considerations from the start, wonder how that would play out in a postgres-centric architecture 🤔

Related Reading