The default architecture for a growing product tends to converge on the same shape: a managed cache, a managed search cluster, and a scheduler or queue for retries. Each is easy to provision and each is documented well enough that a small team can run it without a specialist. The costs of that shape are less visible than the provisioning, and they show up later as latency, spend, and operational surface area.

This article is about when consolidation into a single Postgres instance is a reasonable engineering decision, how to evaluate it with measurements rather than opinions, and where the approach genuinely fails.

The conventional wisdom and where it is incomplete

The standard advice is to use managed services for caching, search, and retries because they scale independently and do not require a senior engineer to operate. The pitch is accurate as far as it goes. A managed cache handles eviction and failover. A managed search cluster handles sharding and replication. A workflow service or scheduler handles retries with a durable state machine.

The incompleteness is in the accounting. Three managed services mean three sets of credentials, three dashboards, three scaling models, three failure modes, and three separate bills. Cross-availability-zone traffic, connection churn, and per-request pricing are all real line items. The operational cost is harder to quantify but usually larger: a new engineer has to learn three systems before they can debug a single slow request.

The conventional advice also assumes that complexity is outsourced rather than moved. It is moved. It ends up in runbooks, in alerting rules, and in the mental model each engineer has to carry.

The assumption worth questioning is narrower than "managed services are bad." It is this: that caching, search, and retries require three separate systems. Postgres can perform all three functions at moderate scale, and the question is whether your scale is moderate.

A typical failure chain

Consider a team that provisions a managed cache, a managed search cluster, and a scheduler on a small instance, then grows traffic by two orders of magnitude over a few weeks. Three failure modes recur.

Cache stampede. A popular page is shared widely and many users hit the same uncached key within a short window. If the cache is sized smaller than the working set and uses an eviction policy that evicts entries still in demand, the misses fall through to the database simultaneously. The managed dashboard often reports a healthy aggregate hit rate, because the stampede is concentrated on a small number of keys and short in duration. Aggregate hit rate is a poor detector for this. Per-key miss rate and database query rate are better signals.

Search skew. A search cluster created with default shard counts can develop hot shards when one category or tenant grows faster than the others. Latency rises for queries routed to the hot shard while cluster-level health indicators stay green. The symptom is a latency distribution with a long tail that does not correlate with total query volume. Shard-level latency percentiles are the metric that exposes it.

Retry avalanche. A retry loop with linear backoff and no jitter, pointed at a downstream dependency that has begun throttling, will synchronize its retries and amplify load. The scheduler instance saturates, the database connection pool is consumed by retry transactions, and unrelated request paths fail. This is a design problem, not a capacity problem, and adding capacity makes the synchronized burst larger rather than smaller.

The common response to all three is to increase capacity. That raises the ceiling on the next incident without changing its shape.

What Postgres actually provides

Postgres is a general-purpose database with an extension system. Several capabilities that are often assumed to require separate services are available inside it, with different trade-offs than the dedicated systems.

  • Full-text search. Postgres ships with built-in full-text search using tsvector and tsquery, supporting stemming, ranking via ts_rank, and GIN or GiST indexes. This is available in core Postgres and requires no extension. Third-party extensions exist that add BM25-style ranking and additional tokenization options; their performance and feature set should be evaluated against your own corpus rather than assumed.
  • Scheduling. The pg_cron extension runs SQL on a cron schedule inside the database. It is a scheduler, not a workflow engine: it does not provide step functions, branching, or cross-system orchestration. For retry loops that operate on rows already in the database, it removes a network hop and lets the retry participate in the same transaction as the state change.
  • Serving hot rows from memory. Postgres caches pages in shared buffers, and the operating system page cache holds additional data. You can also use UNLOGGED tables for data that is safe to lose on crash, which avoids WAL write overhead. There is no separate in-memory table type in stock Postgres; the practical approach is to size shared_buffers appropriately, keep the hot working set smaller than RAM, and use partial or covering indexes so hot queries touch few pages.
  • Connection management. Postgres uses one process per connection. A connection pooler in front of the database, or the built-in pooler in newer managed offerings, keeps the number of backend processes bounded while allowing many application connections. The default max_connections is 100, and raising it consumes memory per backend, so pooling is usually the better answer than raising the limit.

The mental shift is to treat the database as a compute platform rather than a passive store. The constraint is that all of these workloads now compete for the same CPU, memory, and I/O. That is the central trade-off and it deserves explicit planning.

Where the consolidation argument is strongest

Consolidation tends to be a good fit when several of the following hold:

  • The working set for cache and search fits comfortably in RAM on a single instance, with headroom for the database's own buffers and for query execution.
  • The team has no dedicated database or infrastructure specialist, so the operational cost of three systems is paid by application engineers.
  • The dominant workload is read-heavy with a modest write rate, and query patterns are known well enough to index deliberately.
  • Availability requirements tolerate a single primary with a standby, rather than requiring independent failure domains for each subsystem.
  • The retry logic operates on data already in the database, so keeping it in the same transaction removes reconciliation work.

The argument is weakest when the subsystems need to fail independently. If losing the database takes down search and caching anyway, consolidating them does not reduce your blast radius; it just removes the illusion that they were separate.

Where it breaks down

Be specific about the limits, because they are reached faster than the enthusiasm suggests.

  • Working set larger than RAM. Once the hot data exceeds memory, the database begins reading from disk for what was previously an in-memory lookup. Latency degrades non-linearly, and no amount of indexing fixes a working set that does not fit.
  • Analytical or high-cardinality search. Ranking quality for typo tolerance, synonym expansion, and semantic similarity is generally better in a dedicated search engine. If search relevance is a product differentiator rather than a lookup, a dedicated engine is the right tool.
  • Write-heavy ingest. Search indexes and caches that receive high write rates compete with transactional writes for I/O and for autovacuum capacity. Postgres handles high write rates, but the tuning required is real and the failure mode is bloat and index degradation.
  • Independent scaling. If search needs ten times the CPU of your transactional workload, running them on the same instance means paying for the maximum of the two on one machine, or accepting that search will be slow during transaction peaks.
  • Multi-region requirements. A single primary does not provide geographic redundancy. Managed services often provide cross-region replication as a configuration option; replicating Postgres across regions is possible but is a project.

A reasonable pattern is to run the consolidated stack first and offload a subsystem only when a specific measurement crosses a threshold you defined in advance.

How to measure whether consolidation helps

Do not estimate. Instrument.

Latency. Enable pg_stat_statements and record the mean and p95 execution time per query shape before and after any change. For end-to-end latency, measure at the application boundary, not the database, because the network hop to a managed service is part of what you are evaluating.

CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

SELECT
  calls,
  round(mean_exec_time::numeric, 2) AS mean_ms,
  round(total_exec_time::numeric, 2) AS total_ms,
  query
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;

Reset the statistics before a controlled comparison with SELECT pg_stat_statements_reset(); so the window is clean.

Cache effectiveness. If you are considering moving a cache into Postgres, first measure what the existing cache is actually saving. Log cache hits and misses per key prefix, then compute the miss rate and the cost of a miss:

miss_rate = misses / (hits + misses)
cost_of_miss = p95_database_query_time_for_that_key
expected_savings = miss_rate * cost_of_miss

If the miss rate is low and the underlying query is already fast, the cache is not doing much work and moving it into the database changes little.

Search relevance and latency. Build a small evaluation set of representative queries with expected top results. Measure recall at k and latency for the current engine and for the candidate. Relevance cannot be measured by latency alone, and a faster engine that returns worse results is not an improvement.

Cost. Compare on the same basis. Include compute, storage, network egress, cross-AZ traffic, backup storage, and any per-request charges. For the consolidated option, include the cost of a standby instance and a larger instance size to absorb peak combined load. A single-instance comparison against a multi-service bill is not a fair comparison.

Operational load. Count pages per week attributable to each subsystem over a month before the change. This is the metric most often left out and often the one that decides the question.

A worked sizing example

The following is illustrative, not a benchmark. Suppose the following assumptions hold for a read-heavy product:

  • Total dataset: 200 GB.
  • Hot working set (rows accessed in a typical hour): 40 GB.
  • Peak read rate: 8,000 queries per second, of which 90 percent are simple key lookups served from cache or from a covering index.
  • Peak write rate: 300 rows per second.
  • Search index size: 15 GB.

Sizing the instance:

  • Shared buffers: a common starting point is 25 percent of RAM. On a 64 GB instance that is 16 GB, which covers a large fraction of the hot working set.
  • Operating system page cache: the remainder of RAM after shared buffers and the Postgres process overhead, roughly 40 GB, holds the rest of the hot set and the search index.
  • Write volume: 300 rows per second at an average row size of 2 KB is 600 KB per second, or about 52 GB per day of WAL before compression. That is a substantial but manageable rate for a single instance with fast storage, and it determines your backup and replication bandwidth requirements.

If the hot working set were 120 GB instead of 40 GB, the same instance would be reading from disk for a large share of lookups and the consolidated design would be the wrong choice. The sizing exercise is the decision, not an afterthought.

Decision checklist

Work through these in order. Stop at the first one that fails.

  1. Does the hot working set plus index size fit in RAM on a single instance with at least 30 percent headroom?
  2. Is search relevance a lookup problem rather than a ranking problem?
  3. Can the workload tolerate a single primary with a standby, or does each subsystem need an independent failure domain?
  4. Is the write rate low enough that autovacuum can keep up without tuning that your team understands?
  5. Do you have a measurement for the current latency, cost, and page volume per subsystem, so the comparison is empirical?
  6. Do you have a rollback plan that does not require downtime?

If all six pass, a consolidation trial is reasonable. Run it on one subsystem at a time, keep the old system in place as a fallback, and define the threshold that would end the trial before you start.

Migration mechanics

Moving a cache into the database is the easiest of the three, because a cache can be wrong without corrupting data.

A dual-write approach works as follows. Write to both the old cache and the database for a period. Read from the old cache first, and on a miss read from the database and populate both. Track the database read rate as the primary metric. When the database read rate has been stable for a full traffic cycle, including your peak, switch reads to the database first and keep the old cache as a fallback. Remove the fallback when you are confident.

For search, the migration is a backfill plus a dual-write. Backfill the new index, then write to both indexes while you compare result sets on a sample of production queries. Switch reads only after relevance and latency both meet the threshold.

For retries, the migration is a rewrite rather than a move. The important properties are that retries are idempotent, that backoff includes jitter, and that a retry that fails permanently is recorded rather than silently dropped. Moving the retry into the database transaction is valuable only if those properties already hold.

Frequently asked questions

Does Postgres full-text search replace a dedicated search engine?

For structured lookups and straightforward keyword search on a corpus that fits in memory, core Postgres full-text search is often sufficient. For typo tolerance, synonym expansion, complex relevance tuning, or semantic search, a dedicated engine is generally the better tool. Measure recall on a representative query set before deciding.

How do I avoid connection exhaustion?

Use a connection pooler between the application and the database. Keep the number of backend processes bounded, size the pool to the number of CPU cores times a small multiplier, and set the client-side connection limit separately from max_connections. Raising max_connections directly increases memory usage per backend and is usually the wrong fix.

What happens to availability if everything runs on one instance?

You lose independent failure domains. A primary with a streaming standby and an automated failover tool gives you recovery, but it is not the same as three systems failing independently. If your availability target requires that search stay up while the database is down, consolidation is not appropriate.

Is pg_cron a replacement for a workflow engine?

No. It runs SQL on a schedule. It does not provide branching, cross-system orchestration, or durable multi-step state. It is a good fit for periodic maintenance and for retries that operate on rows already in the database.

How do I know when to offload a subsystem?

Define the threshold before you start. For example: offload search when p95 search latency exceeds a value you have committed to, or when the index no longer fits in memory. A threshold defined in advance prevents the decision from being made during an incident.

Actionable next step

Run the following in a read replica or a staging copy of your database, then compare the output against your current cache and search latency numbers:

CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

SELECT
  calls,
  round(mean_exec_time::numeric, 2) AS mean_ms,
  round((total_exec_time / calls)::numeric, 2) AS avg_ms,
  query
FROM pg_stat_statements
WHERE calls > 100
ORDER BY total_exec_time DESC
LIMIT 20;

The queries at the top of this list by total time are where your database is actually spending effort. If the top entries are the same queries your cache and search services exist to avoid, you have a concrete basis for the consolidation decision. If they are not, the cache and search services are earning their cost and the discussion is settled.