Database Capacity Planning: A Practical Guide

Plan for growth before you hit walls. This guide covers growth forecasting, compute and storage sizing, IOPS requirements, and cloud vs on-prem decisions.

published: reading time: 30 min read author: GeekWorkBench updated: June 17, 2026
Quick Summary

Database capacity planning uses measured workload and growth trends to guide compute, memory, storage, I/O, and connection sizing. This guide walks through PostgreSQL metrics, storage and pooling trade-offs, cloud considerations, and signals such as disk growth, latency, and connection use. It shows how to turn those measurements into review triggers and provisioning plans, then revisit the assumptions as the workload changes.

Database Capacity Planning: A Practical Guide

Introduction

Capacity planning prevents failures such as running out of disk, saturating CPU during a launch, or exhausting database connections. It helps teams size infrastructure for current demand while using observed workload and growth trends to prepare for future load.

This guide covers compute, memory, storage, connections, and cloud-versus-on-premises trade-offs, with practical estimates and monitoring approaches. It also examines how incorrect forecasts and weak alerts can compound into production failures.

Sizing Compute (CPU)

CPU sizing depends on query complexity and concurrency requirements. A database running simple CRUD operations needs less CPU than one running complex analytical queries.

For CPU, consider both per-core speed and the parallelism available to your workload. Some PostgreSQL operations can use parallel workers, but many query plans and workloads do not scale across every core. Benchmark representative queries before choosing between fewer faster cores and more cores.

As a starting point, size production compute from measured concurrency and representative load tests. Sustained high CPU can signal a need to optimize queries or add compute, but review latency, wait events, and utilization together before upgrading.

Sizing Memory

Memory is where PostgreSQL caches data and does its work. More memory means fewer disk I/O operations, which are orders of magnitude slower than memory access.

The key metric is effective cache size:

SHOW effective_cache_size;

This setting is a planner estimate of the cache available to a query; it does not allocate memory. Choose it from the memory PostgreSQL and the operating system can realistically use for cached data. A value around 50-75% of RAM is a common starting estimate, not a fixed target. If you have 32GB RAM, 24GB may be reasonable after accounting for the workload and other processes.

PostgreSQL’s default shared_buffers is commonly 128MB. For a dedicated 32GB database server, 8GB is a common starting point; benchmark before increasing it, since PostgreSQL also relies on the operating system cache.

work_mem is a limit for each eligible query operation, such as a sort or hash table. A query can have several such operations, and concurrent queries multiply the possible memory use. maintenance_work_mem applies to maintenance operations such as VACUUM and CREATE INDEX; size both for concurrency as well as individual operations.

Sizing Storage

Storage sizing requires estimating current data size, growth rate, and headroom.

Current and Projected Size

Calculate current table and index sizes using the queries from the Data Growth Modeling section above. Then project forward by estimating your growth rate.

Growth rates depend on your application type. A typical pattern:

Application Type Growth Rate Notes
SaaS with expanding user base 10-20% per month Users + data per user compound
Content/media platform 30-50% per month Media-heavy, lower per-user overhead
IoT/data ingestion 50-100% per month High write volume, minimal reads
Gaming/social platform 15-30% per month Viral growth phases common

Once you have your monthly growth rate, calculate how much space you will need in 3, 6, and 12 months. Add headroom based on bloat, maintenance, and unexpected spikes. For example: a 500GB database growing at a compounded 20% per month reaches about 1.5TB in 6 months and 4.5TB in 12 months, before adding headroom. Treat 70% utilization as a possible review trigger, then set the actual threshold using growth rate and storage provisioning lead time.

Storage Type Considerations

Storage type selection determines your database’s I/O ceiling. NVMe/SSD, HDD, and network-attached storage each serve different workload profiles, and the choice comes down to tradeoffs between throughput, latency, cost, and capacity.

NVMe and SSD can provide lower latency and higher random I/O rates than spinning disks, though actual performance depends on the device, workload, and configuration. NVMe connects over PCIe and can offer more parallelism than SATA SSDs. For a write-heavy or latency-sensitive workload, benchmark the intended storage rather than assuming advertised maximums will match database performance. SSD storage generally costs more per GB than HDD.

HDD still makes sense for specific workloads. Large analytical databases running sequential scans across many terabytes benefit from HDD’s sequential read throughput and lower cost per GB. If your workload is read-heavy, mostly sequential, and capacity-hungry rather than IOPS-hungry, HDD can be 5-10x cheaper per GB. The problem is random I/O—database writes, index updates, and VACUUM activity all generate random I/O patterns that spinning disks handle poorly. Mixing sequential analytical scans with random OLTP I/O on the same HDD is a good way to watch your query times climb.

Network-attached storage in cloud environments adds latency. The path from database to storage goes through the network stack, costing you 0.5-2ms compared to local NVMe. For latency-sensitive OLTP, that matters. For analytical or batch workloads where queries run for minutes or hours, the added latency is effectively zero. Cloud storage also has throughput caps that can throttle large sequential scans.

Storage Type Performance profile Best For
NVMe (local) High random I/O and low latency, device-dependent OLTP, mixed read/write
SATA SSD General-purpose random and sequential I/O General purpose
HDD (7200 RPM) Low random I/O, economical sequential reads Sequential analytics
Cloud network storage Limits and latency depend on service and tier Managed cloud deployments

Cloud environments have their own gotchas. AWS gp3 volumes give you 3,000 IOPS baseline at 125 MB/s throughput but can run out of steam at high block sizes. gp2 volumes burst to 3,000 IOPS but degrade as you chew through the burst budget. Provisioned IOPS (io1/io2) volumes deliver guaranteed performance at 3-5x the cost of gp3. On Google Cloud, regional SSD performs best but adds network latency compared to zonal SSD. Always test storage performance with fio or pg_test_fsync before committing to a volume type in production.

For new deployments, compare local NVMe with the managed cloud storage tiers available in your environment. Reserve HDD for workloads where sequential throughput and capacity matter more than random I/O. Confirm the choice with representative benchmarks and account for durability, failover, and operational requirements.

IOPS Requirements

IOPS (Input/Output Operations Per Second) measures storage throughput. Database workloads are I/O intensive, especially with poor cache hit rates. A database with a 90% cache hit rate still needs to write every transaction to disk through the WAL.

Estimate I/O needs by profiling your actual workload with operating-system tools and PostgreSQL statistics. PostgreSQL 16 and later provide pg_stat_io; PostgreSQL 17 also provides pg_stat_checkpointer. pg_stat_bgwriter remains available, but its counters differ by PostgreSQL version. Capture read/write operations, bytes, latency, and checkpoint behavior over representative peak periods.

  • Data and index reads/writes: affected by cache behavior, query patterns, and maintenance
  • WAL generation and write latency: track with PostgreSQL WAL statistics and storage metrics
  • Checkpoint activity and temporary I/O: include these in peak-load measurements

There is no reliable conversion from transaction rate or WAL volume directly to storage IOPS; write batching, page writes, caching, and checkpoints affect the result. Measure a representative workload and retain enough headroom for observed peaks.

Cloud providers specify IOPS for their storage volumes. AWS gp3 volumes deliver 3,000 IOPS baseline at 125 MB/s throughput. Provisioned IOPS (io2) volumes can reach 256,000 IOPS. Make sure your volume type supports your workload’s I/O requirements, and test with pg_test_fsync before going to production.

Connection Pool Sizing

PostgreSQL connections have process and shared-memory overhead and compete for CPU. A large number of idle connections can add overhead without increasing useful query concurrency.

Direct Connection Limits

max_connections sets the absolute maximum number of PostgreSQL connections. The default is typically 100, though deployments may choose a different limit. More is not always better: work_mem is consumed by eligible query operations, not reserved once per connection, while each connection also adds process and shared-memory overhead. PostgreSQL documents that some resources scale with max_connections.

The right max_connections value depends on your pooler configuration, expected active query concurrency, and available memory. Keep capacity for administrative and replication connections, and validate the setting under load rather than treating one value as suitable for every 4-core server.

Setting max_connections too high relative to your available memory causes PostgreSQL to swap or OOM. Setting it too low causes “too many connections” errors that take down applications. If you find yourself consistently hitting the limit, add PgBouncer before raising max_connections. A pooler lets 200 application connections share 20 actual database connections.

Connection Poolers

For most production workloads, use a connection pooler like PgBouncer or Pgpool-II. Without a pooler, each application instance maintains its own database connections, and at scale this creates a problem: 50 application servers each opening 20 connections is 1,000 PostgreSQL connections, most of them idle at any given moment. A pooler lets 1,000 application connections share 20-50 actual database connections efficiently.

PgBouncer is the most common choice. It operates at transaction level by default, meaning a client connection only occupies a real PostgreSQL connection during an active transaction. This lets hundreds of clients share dozens of real connections. The downside: session-level features break. You cannot use prepared statements with the default prepared statement caching, and SET statements that persist across transactions do not work. For most web applications this is fine.

Pgpool-II offers more features including built-in query caching, load balancing across replicas, and session-level pooling. It adds complexity and higher memory overhead. Use it when you need automatic query routing or caching at the pooler layer.

PgCat is a newer option written in Rust, designed for high-performance connection multiplexing with less overhead than PgBouncer. It supports session and transaction modes and has lower memory footprint. Consider it for greenfield deployments on modern infrastructure.

The pooler you choose matters less than using one. Direct database connections at scale cause connection exhaustion, CPU overhead from context switching, and memory waste from idle connections.

Pool Size Calculation

There is no universal pool-size formula. Start with a pool small enough to keep active database concurrency within the server’s measured capacity, then benchmark representative traffic. OLTP with short transactions and analytical workloads with long queries may need different limits. Monitor queue time, active sessions, and database latency when tuning.

Cloud vs On-Premises Tradeoffs

The cloud versus on-premises decision affects capacity planning significantly.

Cloud gives you elasticity and variety—you can scale up when needed and choose instance types optimized for compute, memory, or I/O. Managed services like RDS and Cloud SQL handle some operational burden. But at large scale, cloud database costs become significant. A $50,000/month database in the cloud might cost $10,000/month on-premises. You also have less control over kernel parameters and storage optimization, and IOPS often costs extra.

Many organizations use a hybrid approach: core production data on-premises or in a private cloud, with burst capacity or read replicas in public cloud for traffic spikes.

Security and Compliance Notes

Capacity forecasts can reveal customer activity, business growth, or infrastructure weaknesses. Limit access to detailed dashboards and exported plans, aggregate tenant-level usage when individual identities are not needed, and keep credentials and connection strings out of planning documents. Set a retention period for forecast exports instead of leaving them in shared storage indefinitely.

Before choosing regions or storage tiers, check data residency, encryption, backup, and audit-retention requirements. Those rules affect replica placement, disaster-recovery capacity, WAL storage, and how long backups and logs must be kept. Include these security and compliance needs in the capacity model, and restrict infrastructure changes to reviewed plans with an audit trail.

Capacity planning is not a one-time exercise. You need ongoing monitoring to predict when you will need to scale.

Key Metrics to Track

  1. Disk utilization: If consistently above 70-80%, plan storage expansion.
  2. CPU utilization: Sustained above 70% indicates underpowered CPU.
  3. Memory pressure: Check for swap usage, which indicates insufficient RAM.
  4. Connection counts: Watch for approaching max_connections.
  5. Query latency trends: Slow queries often precede capacity crises.

Capacity Triggers

Set thresholds that force a capacity review before you hit the wall. These differ from alerts—alerts tell you something is already broken, while triggers give you lead time to act.

For a production PostgreSQL database, these are the numbers I come back to:

Disk above 70%. At 70% disk usage, you have roughly 30% headroom. For a database growing 10GB per day, that headroom is gone in three days. Set your trigger at 70% so you have 60 days to provision new storage. When you hit 80%, you are in emergency mode—execute the plan you made at 70%.

CPU above 80% sustained. Brief spikes are normal. Sustained 80%+ means queries are competing for cycles and latency is degrading. Pull pg_stat_statements to check whether the load comes from a few bad queries or general growth. Bad queries: optimize. Growth: scale compute.

Memory pressure. Swap usage is the clearest signal. If the OS is swapping, shared_buffers and work_mem are undersized. Run vmstat and look for non-zero si values. Even before swap kicks in, low free RAM with heavy disk cache means I/O is suffering.

Connections above 70% of max. Connection exhaustion fails queries with a generic “too many connections” message that is painful to debug. At 70% of max, start evaluating whether to add PgBouncer or raise the limit. At 90%, you are one traffic spike away from outages.

p99 latency trending up. Slow queries are often the first warning of an approaching capacity problem. If your p99 latency is climbing week-over-week, investigate before it becomes a crisis. Set a baseline and alert when p99 exceeds 2x that baseline.

These thresholds compound. A database at 65% disk, 75% CPU, and 60% connections is fine. One at 72% disk, 82% CPU, and 73% connections is circling a failure where multiple resources hit their limits at once. That is the failure mode capacity planning prevents—not any single threshold in isolation.

Practical Capacity Planning Process

flowchart TD
    Start["Start: New workload or growth trigger"] --> Measure["1. Measure current:<br/>Disk, CPU, Memory,<br/>Connections, IOPS"]
    Measure --> Characterize["2. Characterize workload:<br/>Read/write ratio?<br/>Query complexity?<br/>Peak patterns?"]
    Characterize --> Project["3. Project growth:<br/>3, 6, 12 month<br/>trajectory"]
    Project --> Size["4. Size resources:<br/>Compute, Storage,<br/>Memory, IOPS"]
    Size --> Buffer["5. Add 30-50% buffer:<br/>Growth headroom +<br/>Traffic spikes"]
    Buffer --> Monitor["6. Monitor trends<br/>Quarterly review:<br/>Adjust projections"]
    Monitor --> Adjust["Adjust projections"] --> Project

A practical approach to capacity planning: baseline your current usage, characterize your workload, project growth, add buffer, and monitor trends. Update projections quarterly.

Production Failure Scenarios

Failure Cause Mitigation
Disk exhaustion mid-operation Underestimated data growth, no monitoring Track disk weekly, trigger review at 60%
CPU saturation during peak Undersized compute for query complexity Profile query CPU, add read replicas before peaks
Memory OOM crashes shared_buffers + work_mem undersized Size effective_cache_size at 75% of RAM
Connection pool exhaustion max_connections too low Deploy PgBouncer, right-size pool
IOPS throttling Cloud volume IOPS limits hit Choose appropriate volume types

Capacity Planning Failure Patterns

Two common failure patterns show why forecasts need to include retention and concurrency assumptions:

Storage exhaustion

Suppose a database forecast includes table growth but omits WAL retention, backups, index growth, or temporary files. Usage can then outpace the forecast, and an alert set close to the disk limit may leave too little time to expand storage. Track each storage consumer, project its growth, and set review triggers using the actual provisioning lead time.

Connection pool exhaustion

As an application adds instances and background workers, each may create its own pool. The aggregate number of database connections can exceed the limit even if each service’s pool looks small. Track pools across services, reserve capacity for administrative and replication connections, and load-test peak concurrency.

These patterns are easier to manage when assumptions are explicit and monitoring tracks both current usage and its rate of change.

Quick Recap Checklist

  • Size CPU from representative load tests; use sustained utilization and latency to trigger review
  • Set effective_cache_size to a realistic estimate of cache available to the planner
  • Tune shared_buffers from a measured baseline while accounting for OS cache
  • Size work_mem for concurrent query operations, not just sessions
  • Size maintenance_work_mem for maintenance workload and worker concurrency
  • Monitor disk usage weekly; trigger capacity review at 70%, expand at 80%
  • Use SSD/NVMe storage for OLTP workloads — HDD cannot handle high IOPS
  • Benchmark pool size against active query concurrency and database latency
  • Add 30-50% headroom above projected storage need for bloat and growth
  • Track disk growth week-over-week to establish accurate growth rate
  • Separate WAL onto dedicated volume to isolate backup I/O from production
  • Monitor connection utilization — alert at 70% of max_connections
  • Use PgBouncer or similar for connection pooling in production
  • Plan compute and storage upgrades when metrics exceed 70-80% sustained
  • Review and adjust capacity quarterly based on actual growth patterns

Interview Questions

1. Your PostgreSQL database is consuming 400GB of a 500GB disk. Growth rate is 10GB/day. A new customer onboarding next quarter will add 50% more data volume. What's your capacity plan?
The critical path is time-to-capacity: 400GB used, 100GB free, 10GB/day growth means 10 days before disk is full. This is already critical. Immediate action: provision additional storage within 48 hours. For the new customer, calculate: 50% more data volume means 200GB additional at current growth rate, plus the 10GB/day ongoing growth. That's 300GB+ needed within 30-60 days. My plan: Week 1: provision storage to get to 200GB headroom (100GB immediate + 100GB for 10 days growth). Week 2: implement or verify monitoring on table-level storage consumption to identify bloat or unnecessary data. Month 1: provision the larger storage tier accounting for new customer load (add ~300GB). Month 2-3: evaluate read replicas or archiving strategies for older data to reduce primary disk load.
2. How do you calculate the right number of connections for your connection pool?
There is no standard formula that fits every workload. Benchmark representative traffic and choose a pool size that keeps active database concurrency within the server's capacity. Keep max_connections available for the pool plus administrative and replication connections. Monitor pg_stat_activity, application queue time, and query latency; long-lived idle-in-transaction sessions usually point to an application or transaction-management problem. For high connection churn, a pooler such as PgBouncer can multiplex application connections over fewer database connections.
3. What's the difference between IOPS and throughput, and how do you size storage for a write-heavy database?
IOPS measures discrete storage operations per second, while throughput measures data transferred per second. Random page access is often IOPS-sensitive; sequential scans and bulk loads are often throughput-sensitive. Device specifications are only a starting point because actual database performance depends on block size, queue depth, caching, and the cloud storage tier. Measure reads, writes, bytes, and latency under representative load. WAL volume alone does not determine storage IOPS, since writes may be combined or buffered.
4. You need to forecast capacity for a database that will grow from 1TB to 10TB over 2 years. How do you approach this?
Three-step approach: First, establish the growth curve. Pull 90 days of historical data from pg_stat_database (tup_returned, tup_fetched, tup_inserted, tup_updated, tup_deleted) and pg_stat_bgwriter to understand write volume and query patterns. Calculate growth rate per week. Second, model the growth curve: linear growth (e.g., 1TB to 5TB then to 10TB = ~5% per week), exponential growth (doubling periods), or step-function growth (acquisition-driven). For 1TB to 10TB in 24 months, average growth is ~375GB/month or ~12.5GB/day. Third, plan provisioning triggers: at 70% of current storage tier (e.g., provision new tier at 700GB for 1TB volume), provision the next tier 60 days before you hit 90% of current tier. Budget for 3x the storage you think you need in year 2 because growth often accelerates.
5. Your organization is moving from on-premises PostgreSQL to Amazon RDS. How does capacity planning change for a managed database service?
Managed database services change capacity planning in several ways: (1) Storage is elastic but IOPS may be tied to instance size — you need to verify IOPS limits match your workload; (2) Memory management changes — AWS manages shared_buffers and you can't tune it directly; (3) CPU credits apply to burstable instances — check if your workload sustains high CPU or relies on bursts; (4) Network limits tied to instance type — larger instances have higher network throughput; (5) Connection pooling is still your responsibility (RDS Proxy helps but doesn't eliminate PgBouncer need); (6) Backup and maintenance windows are managed but affect availability; (7) Max connections scales with instance size — plan for connection pooling regardless. For RDS specifically: monitor CloudWatch metrics for CPU, memory, storage, and IOPS; set up alerts before hitting instance limits. Use read replicas for read scaling — they handle read traffic but not write scaling.
6. A database query that runs in 100ms on a dev machine takes 10 seconds in production with similar data volumes. What capacity planning factors could explain this?
Query time differences between dev and production at similar data volumes usually indicate: (1) Insufficient memory — query is spilling to disk due to work_mem being too low; check work_mem setting and increase per-query memory allocation; (2) Different query plan — dev might have fresh statistics while production has stale ones; run ANALYZE on production; (3) Shared buffers — production cache hit ratio might be low because dev fits in memory while production doesn't; (4) Connection pool configuration — production might have connection queueing while dev has direct connection; (5) Network latency — prod might be cross-region or higher network latency; (6) CPU contention — prod might have CPU contention from other processes; (7) Disk I/O — production disks might be slower (cloud volumes vs local SSD) or saturated. The first diagnostic step is EXPLAIN (ANALYZE, BUFFERS) on both systems to compare actual execution plans and cache hit rates.
7. You need to size storage for a database that will receive 100GB of data per day. How do you plan for a 90-day retention period with growth?
Base calculation: 100GB/day × 90 days = 9TB base storage. Add factors: (1) Index overhead — typically 20-30% additional storage for indexes; (2) Write amplification — VACUUM generates dead tuples; plan for 10-20% dead tuple headroom during high-write periods; (3) WAL volume — writes generate WAL at roughly 25-50% of data volume; allocate separate storage for WAL if using replication; (4) Growth rate: if data grows 10% month-over-month, the 90th day load is ~30% higher than day 1; (5) Temp space — sorting and hash joins use temp files; allocate 10-20% for temp space. Total: ~14-16TB at day 90. Provision storage to reach 80% at 60 days (giving 30 days headroom). Cloud: consider automated scaling but verify scaling speed — some cloud volumes take minutes to hours to expand.
8. How do you size effective_cache_size and why does it matter for query performance?
effective_cache_size is a planner estimate of the cache available to a query; it does not allocate or reserve memory. Choose a value based on shared_buffers, operating-system cache, and memory used by other processes. The planner uses the estimate when comparing scan and join costs, so an unrealistic value can lead to poor estimates. A value around 50-75% of RAM is only a common starting range; validate it against the host and workload.
9. What is the relationship between work_mem, maintenance_work_mem, and query performance?
work_mem is the base memory limit for each eligible query operation, such as a sort or hash table. A single query may use several operations, and concurrent sessions can do the same, so dividing available RAM by the number of sessions alone can still overcommit memory. Start conservatively, observe temporary-file activity, and raise it only after accounting for peak concurrency and hash operations. maintenance_work_mem applies to maintenance work such as VACUUM and index creation; allow for concurrent maintenance workers when sizing it.
10. A database shows 99% cache hit ratio but p99 latency is high. How do you diagnose this?
High cache hit ratio with high latency is a classic sign of a bottleneck other than memory. Diagnosis order: (1) Check CPU — high CPU usage means query complexity is the bottleneck, not data access; (2) Check I/O latency — iostat on the host shows disk queue depth and latency; if latency > 1ms for SSDs, the disk might be saturated; (3) Check network — if the database is remote, network latency adds to query time; (4) Check lock contention — SELECT * FROM pg_stat_activity WHERE wait_event_type IS NOT NULL shows queries waiting for locks; (5) Check connection queueing — if max_connections is near limit, queries wait for a connection; (6) Check for long-running queries — pg_stat_activity shows queries with high runtime. The question to ask: where does time go? Use EXPLAIN (ANALYZE, BUFFERS) on representative slow queries to identify which operator (sort, hash, seqscan) is taking the most time.
11. How does PostgreSQL's bgwriter configuration affect write performance and when should you tune it?
The background writer writes dirty shared buffers ahead of demand, but checkpointing also controls how dirty pages are persisted. In PostgreSQL 17, check pg_stat_io for I/O activity and pg_stat_checkpointer for checkpoint statistics; pg_stat_bgwriter exposes a different set of counters than in earlier versions. Use these metrics with storage latency and workload measurements before changing background-writer or checkpoint settings. A high write rate by client backends can point to checkpoint or I/O pressure, but does not by itself show that changing one background-writer setting will help.
12. You need to plan capacity for a database that will grow from 1TB to 50TB over 3 years. Walk through the provisioning strategy.
Three-year provisioning strategy: Year 1: Start with 2TB storage (1TB data + 1TB headroom/growth). Monitor growth monthly; provision additional storage when usage exceeds 70%. Year 2: At 1TB year-1 growth rate, provision to 4-5TB. Add read replicas if read traffic has grown. Consider partitioning large tables by time to improve query performance and enable archival. Year 3: At this scale (10TB+), evaluate sharding or moving hot data to a faster tier. Ongoing: monitor week-over-week growth rate; provisioning cycle should be 60-90 days ahead of consumption. Key metrics to track: disk growth per week, query latency trends, cache hit ratio. Tools: Grafana dashboards showing disk usage over time with projection lines. Budget: cloud storage at scale is significant cost — consider lifecycle policies to archive old data to cheaper storage.
13. What capacity planning considerations apply when enabling PostgreSQL streaming replication?
Replication adds several capacity considerations: (1) Network bandwidth — replica must receive all WAL; for a 10GB/day write database, replica needs 10GB/day network throughput; (2) Storage on replica — replica stores the same data as primary; for 10TB primary, you need 10TB replica; (3) WAL accumulation — if replica falls behind, WAL files accumulate on primary; primary disk can fill up if replica is down for hours; (4) Replica apply lag — replica applies WAL sequentially; large transactions cause replica lag; (5) Connection capacity — max_connections on replica must handle read traffic; (6) Memory pressure — replicas have same shared_buffers as primary; under read load, memory pressure differs. Best practice: size replica with same or slightly less compute than primary (primary handles writes, replica handles reads); ensure network between primary and replica has sufficient bandwidth for WAL streaming; monitor pg_stat_replication for lag.
14. How does changing the PostgreSQL max_connections setting affect capacity planning?
max_connections caps simultaneous database connections, and PostgreSQL allocates some resources based on this setting. It does not reserve work_mem once per connection; eligible query operations can each use that memory while they run. Set the limit from expected active concurrency, pool configuration, and the need for administrative and replication connections. Managed services derive limits and allow configuration differently, so check documentation for the specific engine and instance class. Pooling is usually preferable when many application clients share a smaller amount of useful database concurrency.
15. You are planning to use PostgreSQL logical replication for a multi-tenant saas application. What capacity considerations apply?
Logical replication capacity considerations: (1) CPU overhead — logical replication runs as background workers that decode WAL and apply changes; each publisher and subscriber consumes CPU; (2) Network — logical replication sends row-level changes, not pages; for write-heavy workloads, network usage can be higher than physical replication; (3) Disk — the wal_sender and wal_receiver processes generate temporary files during large transactions; (4) Memory — subscriptions maintain replication state in memory; large tables with many changes can increase memory usage; (5) Schema changes — logical replication does not replicate DDL changes automatically; each tenant schema update must be applied manually or via separate mechanism; (6) Latency — logical replication is asynchronous with typical lag of milliseconds to seconds; not suitable for synchronous cross-region writes. For multi-tenant: each tenant as a separate publication provides isolation but increases overhead; consider row-level security for simpler multi-tenant rather than separate publications.
16. How do you factor backup and disaster recovery into capacity planning?
Backup and DR add capacity considerations: (1) Backup storage — full pg_dump or base backup + WAL takes storage; for a 10TB database, full backups are 10TB each; maintain multiple point-in-time recovery images; (2) Backup I/O — pg_basebackup during production hours competes with normal I/O; schedule during low-traffic windows; (3) WAL volume — PITR requires WAL archiving; WAL generation rate × retention period = WAL storage needed; a database doing 100GB/day writes might generate 30-50GB/day WAL; (4) Restoration time — for DR, restoration speed matters; a 10TB database might take 2-4 hours to restore; this affects RTO; (5) Network for DR copies — copying backups to offsite storage uses bandwidth; (6) Point-in-time recovery testing — needs capacity on DR site to run the restored database. Plan: allocate backup storage at 2-3x the database size for retention; ensure WAL volume is accounted for in storage provisioning; test restore times annually.
17. What is the relationship between table partitioning and capacity planning?
Partitioning helps capacity planning in several ways: (1) Improves query performance on large tables by limiting scans to relevant partitions — reduces I/O and memory pressure; (2) Enables data lifecycle management — older partitions can be moved to cheaper storage or archived; (3) Makes vacuum and indexing more efficient — each partition is vacuumed separately, reducing autovacuum overhead; (4) Improves availability — a corrupted partition doesn't affect the entire table; (5) Simplifies capacity planning — you can size each partition independently based on expected data growth for that time period. Trade-offs: more partitions mean more partition management overhead, more entries in pg_class, and potentially more planning overhead for queries that span many partitions. For time-series data (common in capacity planning), range partitioning by month or quarter is standard. For multi-tenant applications, list partitioning by tenant can isolate tenants but adds management complexity.
18. How does the random_page_cost parameter affect capacity planning decisions?
random_page_cost is the planner's estimate of the cost of a non-sequential page fetch from disk. Default is 4.0 (assuming disk is 4x slower than sequential). For SSD/NVMe storage, you should set random_page_cost to 1.1 or close to seq_page_cost (1.0). Why it matters: the planner uses this value to decide between index scans and sequential scans. If random_page_cost is set too high (appropriate for spinning disks but not for SSD), the planner may prefer sequential scans even when indexes would be faster. Setting it correctly for SSD: ALTER SYSTEM SET random_page_cost = 1.1. This tells the planner that random access is only slightly more expensive than sequential, so it will use indexes more aggressively. For capacity planning: if you have mixed storage (SSD for hot data, HDD for cold data), use tablespaces to separate them and set random_page_cost per tablespace.
19. A startup expects 100x growth in users over 12 months. How do you stage the database infrastructure scaling?
Staged scaling approach: Month 1-3 (current scale): Single primary database with vertical scaling (instance size up to largest practical). Optimize queries, add connection pooling, tune autovacuum. Start read replica for read scaling. Month 4-6 (10x scale): Add read replicas (2-3 replicas for read traffic). Begin vertical scaling toward max instance size. Implement caching layer (Redis) for hot data. Consider table partitioning for large tables. Month 7-9 (50x scale): Evaluate sharding if vertical scaling is maxed. Implement read/write splitting at the application layer. Consider moving to a distributed database (CockroachDB, Spanner) if globally distributed. Month 10-12 (100x scale): Sharding is likely necessary. Evaluate managed database services that auto-scale (Amazon Aurora, CockroachDB). Implement multi-region deployment if global users. Key throughout: monitor growth weekly, not monthly. The fastest path to failure is assuming linear growth when growth is actually exponential.
20. What capacity planning metrics should be tracked as leading indicators of impending capacity exhaustion?
Leading indicators (before hard limits hit): (1) Disk growth rate — track week-over-week growth; if trending toward filling current storage in < 60 days, start procurement; (2) Connection count trend — if growing toward 70% of max_connections, increase pooling or max_connections; (3) Query latency p99 trend — if increasing week-over-week, investigate before it becomes critical; (4) Cache hit ratio decline — gradual decline indicates working set growing beyond cache; (5) Replication lag growth — indicates replica can't keep up with primary write rate; (6) CPU utilization trend — if consistently above 70%, plan upgrade; (7) Autovacuum queue — if tables consistently have high n_dead_tup between autovacuum runs, autovacuum is overwhelmed. Set up Grafana dashboards tracking these metrics with 30-day trends and projections. Alert on rate of change, not just absolute values.

Further Reading

Conclusion

Capacity planning is continuous, not a one-time event. The goal is not to predict perfectly—it is to have enough visibility into trends that you are not surprised.

Build monitoring into your routine. Know your growth rate. Understand your workload characteristics. Make capacity decisions based on data, not guesswork.

Good capacity planning happens before anyone notices a problem. You want boring infrastructure—the kind that simply works, reliably, as load grows.

Category

Related Posts

Database Backup Strategies: Full, Incremental, and PITR

Learn database backup strategies: full, incremental, and differential backups. Point-in-time recovery, WAL archiving, and RTO/RPO planning.

#database #backup #recovery

Connection Pooling: HikariCP, pgBouncer, and ProxySQL

Learn connection pool sizing, HikariCP, pgBouncer, and ProxySQL, timeout settings, idle management, and when pooling helps or hurts performance.

#database #connection-pooling #performance

Failover Automation

Automatic failover patterns. Health checks, failure detection, split-brain prevention, and DNS TTL management during database failover.

#database #failover #high-availability