Stage 3 · Build
Database Reliability Engineering
Database Capacity Planning
Storage growth projections, connection limits, and vertical vs horizontal scaling.
Why Capacity Planning Matters
Database capacity exhaustion causes cascading failures. Running out of disk space stops writes. Hitting max_connections rejects new clients. CPU saturation increases latency across all queries. Capacity planning prevents these emergencies by forecasting resource needs.
-- Database size and growth
SELECT
pg_database_size(current_database()) AS db_size,
pg_size_pretty(pg_database_size(current_database())) AS db_size_pretty;
-- Table sizes
SELECT
tablename,
pg_size_pretty(pg_total_relation_size('public.' || tablename)) AS total_size,
pg_size_pretty(pg_relation_size('public.' || tablename)) AS table_size,
pg_size_pretty(pg_indexes_size('public.' || tablename::regclass)) AS index_size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size('public.' || tablename) DESC
LIMIT 10;
-- Connection usage
SELECT
count(*) AS current_connections,
current_setting('max_connections')::int AS max_connections,
ROUND(count(*)::numeric / current_setting('max_connections')::int * 100, 1) AS pct_used;Start every capacity review with these three queries: total size, largest tables, and connection usage. This gives you a snapshot of current state before projecting growth.
Storage Growth Projections
-- Monthly growth rate
SELECT
date_trunc('month', created_at) AS month,
COUNT(*) AS rows_added,
pg_size_pretty(SUM(pg_column_size(*)) OVER (ORDER BY date_trunc('month', created_at))) AS cumulative_size
FROM orders
WHERE created_at > NOW() - INTERVAL '12 months'
GROUP BY 1
ORDER BY 1;
-- Days until disk full (simple projection)
SELECT
current_size AS current_gb,
free_space AS free_gb,
daily_growth_gb,
ROUND(free_space / daily_growth_gb) AS days_until_full
FROM (
SELECT
pg_database_size(current_database()) / (1024*1024*1024) AS current_size,
-- Replace with actual free disk space
50 AS free_space,
-- Calculate daily growth from last 30 days
(pg_database_size(current_database()) - LAG(pg_database_size(current_database()))
OVER (ORDER BY current_date)) / (1024*1024*1024) AS daily_growth_gb
) calc;Project growth at least 6 months ahead. Account for seasonal patterns (Black Friday, year-end). Plan disk replacement or expansion when free space drops below 20%.
Connection Capacity
-- Calculate connection budget
-- max_connections = application_servers * connections_per_server + reserved
-- Example:
-- 10 application servers
-- 25 connections each (via PgBouncer)
-- 3 reserved for superuser
-- Total: 10 * 25 + 3 = 253
-- Verify current allocation
SHOW max_connections; -- 253
SHOW superuser_reserved_connections; -- 3
-- Check PgBouncer pool configuration
-- default_pool_size: 25
-- max_client_conn: 1000
-- Actual DB connections: number_of_databases * default_pool_sizeConnection planning follows a simple formula: application servers times pool size per server plus reserved connections. Never set max_connections based on application thread count — use a connection pooler.
CPU and Memory Planning
| Resource | Rule of Thumb | Warning Sign |
|---|---|---|
| CPU | 1 core per 5-10 concurrent queries | CPU consistently > 80% |
| RAM | shared_buffers = 25% of RAM | High disk reads (low cache hit) |
| Disk I/O | SSD for production databases | IOPS > 90% of disk capacity |
| WAL volume | Size max_wal_size for checkpoint interval | WAL segments accumulating |
-- Cache hit ratio (should be > 99%)
SELECT
sum(blks_hit) AS cache_hits,
sum(blks_read) AS disk_reads,
ROUND(100.0 * sum(blks_hit) / GREATEST(sum(blks_hit) + sum(blks_read), 1), 2) AS hit_ratio
FROM pg_stat_database
WHERE datname = current_database();
-- Work mem pressure: temporary files indicate insufficient work_mem
SELECT
temp_files,
pg_size_pretty(temp_bytes) AS temp_size
FROM pg_stat_database
WHERE datname = current_database();
-- Check for OOM risk: total memory usage vs available
SHOW shared_buffers; -- PostgreSQL cache
SHOW work_mem; -- Per operation, multiply by concurrent operations
SHOW maintenance_work_mem; -- For VACUUM, CREATE INDEXA cache hit ratio below 99% means shared_buffers is too small or working data exceeds available memory. Temporary files indicate work_mem is insufficient for complex queries.
Vertical vs Horizontal Scaling
| Strategy | Pros | Cons | When to Use |
|---|---|---|---|
| Vertical (bigger machine) | Simple, no app changes | Limited by hardware ceiling | First choice, < 10TB data |
| Read replicas | Read scaling, HA | Write scaling not supported | Read-heavy workloads |
| Sharding | Write scaling, unlimited size | Complex, cross-shard queries | Massive scale, > 10TB |
| Connection pooling | Reduces connection overhead | Requires pooler deployment | Always — even small databases |
Vertical scaling is almost always the right first choice. A modern server with 128GB RAM and 32 cores can handle most workloads. Only shard when vertical scaling is no longer economically viable.
Capacity Review Process
- Monthly: Review storage growth, connection usage, and query performance
- Quarterly: Project growth 6 months ahead, compare to current capacity
- Annually: Evaluate hardware needs, plan upgrades or migrations
- Before launches: Estimate new feature impact on database load
- Set alerts: disk > 70%, connections > 80%, CPU > 70% sustained
The goal is not to predict exact usage — it is to avoid running out of resources unexpectedly. Keep 30-40% headroom on disk, connections, and CPU. This buffer absorbs traffic spikes and gives time for planned upgrades.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.