Capacity Planning In Depth
Capacity planning for PostgreSQL is fundamentally about three resources, CPU, memory, and disk I/O, plus one hard ceiling that behaves differently from all three: the connection limit.
Search across all documentation pages
Capacity planning for PostgreSQL is fundamentally about three resources, CPU, memory, and disk I/O, plus one hard ceiling that behaves differently from all three: the connection limit.
This page builds the mental model that connects those resources to the symptoms teams actually observe, so that a scaling decision starts from evidence rather than from reflexively resizing the instance.
CPU capacity governs how much query execution, sorting, and autovacuum work the cluster can perform concurrently.
Memory capacity governs cache effectiveness through shared_buffers and the operating system's page cache, determining how often a read is satisfied from RAM versus disk.
Disk I/O capacity governs how fast the storage layer can serve reads that miss cache and absorb the continuous stream of WAL writes every transaction generates.
The connection ceiling, set by max_connections, behaves unlike the other three because it is a hard cutoff rather than a gradual degradation, and once reached, new sessions are rejected outright rather than merely slowed.
A useful mental model is a restaurant kitchen: CPU is the number of cooks who can work at once, memory is counter space that avoids repeated trips to the walk-in fridge, disk I/O is how fast that walk-in fridge can be accessed, and the connection ceiling is the fixed number of tickets the kitchen printer can physically hold.
Adding cooks does not help if the walk-in fridge is the bottleneck, and a bigger fridge does not help if the printer is jammed, which is exactly why capacity planning starts with identifying which resource is actually constrained.
PostgreSQL exposes the evidence for this diagnosis directly through pg_settings, pg_stat_activity, and host-level tools like iostat, so the data needed to distinguish a CPU-bound from an I/O-bound workload is already available on every cluster.
Sizing decisions made without that evidence tend to default to habit, resizing to a familiar instance class rather than to what the workload's actual resource profile demands.
The four resources interact through a chain that starts with a query and ends with either fast completion or visible user-facing latency.
A query first competes for CPU to plan and execute, then for memory (work_mem) if it needs to sort or hash, and finally for disk I/O if the data it needs is not already resident in shared_buffers or OS cache.
effective_cache_size does not allocate memory itself, it tells the query planner roughly how much cache is available system-wide, which shapes plan choice, such as favoring an index scan over a sequential scan when the planner expects the relevant pages to already be cached.
Under sustained write load, disk I/O and CPU interact through autovacuum, since a table generating dead tuples faster than autovacuum can reclaim them causes both bloated indexes (more I/O per scan) and more CPU spent evaluating rows that will eventually be discarded.
-- One mechanism made concrete: this signal distinguishes
-- "queries are running" from "queries are queued," which
-- is the first fork in any capacity diagnosis.
SELECT count(*) FILTER (WHERE state = 'active') AS active,
count(*) FILTER (WHERE wait_event_type = 'Lock') AS blocked,
count(*) AS total
FROM pg_stat_activity
WHERE backend_type = 'client backend';Connections interact with all three resources indirectly but significantly, because each open backend consumes memory even while idle, and a connection storm from an under-pooled application can exhaust max_connections long before CPU or disk shows any real strain.
A common reasoning mistake is scaling CPU or instance size first, when the actual bottleneck is frequently a missing index or an unbounded connection count, both of which a bigger instance will not fix and will only make more expensive to keep ignoring.
At scale, capacity planning shifts from reactive firefighting to a forecasting discipline that anticipates resource exhaustion before it becomes an incident.
Storage growth is the resource most teams under-forecast, because table and index growth compounds with time-series or event-log workloads in a way that a single point-in-time snapshot does not reveal, making trend-based projection more useful than a static "percent free" alert.
The choice between vertical scaling, read replicas, and horizontal strategies like partitioning or sharding depends on which resource is actually constrained: a CPU- or memory-bound single-writer workload benefits from vertical scaling, a read-heavy reporting load benefits from replicas, and a single table too large to vacuum or index efficiently benefits from partitioning regardless of instance size.
Sharding specifically should be treated as a last resort in this hierarchy, since it introduces cross-shard query complexity and operational overhead that partitioning or read replicas usually avoid while solving the same underlying pressure.
Connection pooling interacts with capacity planning at an architectural level, because a pooler like PgBouncer changes the actual resource being sized from "connections per application instance" to "connections per pool," which is what makes horizontally scaled application tiers (many pods, many serverless invocations) compatible with a fixed max_connections ceiling.
Capacity reviews conducted on a quarterly cadence, correlating table growth, connection peaks, and top query cost, catch the slow drift toward a resource ceiling long before it becomes a page at 3am.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| Vertical scaling (bigger instance) | Simple, no application changes | Cost grows faster than linear at the top end, has a ceiling | CPU- or memory-bound single-writer workloads |
| Read replicas | Offloads read traffic, improves read latency | Replication lag, does not help write throughput | Read-heavy reporting or analytics load |
| Partitioning | Bounds vacuum and index maintenance cost per partition | Query and migration complexity increase | Very large single tables with a natural key (time, tenant) |
| Sharding | Distributes both read and write load horizontally | Cross-shard queries, significant operational overhead | Write throughput that exceeds any single primary, last resort |
shared_buffers can crowd out.CPU for query execution, memory for caching, disk I/O for reads and WAL writes, and the connection ceiling as a distinct hard limit.
PostgreSQL is frequently bound by disk I/O or lock contention, both of which can saturate independently of CPU.
It does not allocate memory itself; it informs the query planner's estimate of available cache, which influences plan choice like index versus sequential scans.
It is a hard cutoff rather than a gradual degradation, rejecting new sessions outright once max_connections is reached instead of merely slowing them down.
A table accumulating dead tuples faster than autovacuum reclaims them causes bloated indexes that cost more I/O per scan and more CPU evaluating discardable rows.
Vertical scaling fits a CPU- or memory-bound single-writer workload, while a read replica fits read-heavy reporting traffic that does not touch the write bottleneck.
It introduces cross-shard query complexity and operational overhead that partitioning or read replicas usually avoid while addressing the same underlying pressure.
It shifts the resource being sized from connections-per-application-instance to connections-per-pool, letting a horizontally scaled app tier stay under a fixed max_connections ceiling.
A single point-in-time snapshot does not reveal compounding growth from time-series or event-log workloads, so trend-based projection catches what a static free-space check misses.
No, workload and data volume change continuously, so a quarterly review cadence is needed to catch drift before it becomes an incident.
Rarely; a missing index or a bad query plan typically dominates over raw instance size, and scaling hardware first often just makes the same inefficiency more expensive.
pg_stat_activity wait events, cache hit ratio, host-level I/O metrics, and connection saturation, so the decision targets the resource that is actually constrained.
Stack versions: This page was written for PostgreSQL 18.4 (stable major 18, maintenance line 17) and PgBouncer 1.x.
Reviewed by Chris St. John·Last updated Jul 15, 2026