Prompt
How do I scale PostgreSQL as my app grows past the first few thousand users?
Latest observation
Scaling PostgreSQL past the “it fits on one box” stage is mostly about reducing unnecessary load, using the database efficiently, and adding read/write capacity only when needed. A good path is usually:
1) Make the single PostgreSQL instance faster first
Before adding complexity, squeeze more out of one node.
Index well
- Add indexes for your most common filters, joins, and sorts.
- Use
EXPLAIN (ANALYZE, BUFFERS)to verify queries actually use them. - Avoid over-indexing: every index slows writes.
Fix slow queries
- Look for:
- sequential scans on large tables
- N+1 query patterns
- queries returning too much data
- unnecessary
ORDER BY,DISTINCT, or subqueries
- Enable
pg_stat_statementsto find top offenders.
Keep transactions short
- Long transactions block vacuum and can cause bloat.
- Avoid holding transactions open while doing app work or calling external APIs.
Tune PostgreSQL basics
Common knobs to review:
shared_bufferswork_memeffective_cache_sizemaintenance_work_mem- autovacuum settings
Don’t guess—measure memory, I/O, and query patterns.
2) Scale the application layer around PostgreSQL
Often the app grows faster than the database.
Use connection pooling
PostgreSQL doesn’t love thousands of direct client connections.
- Use PgBouncer or a similar pooler.
- Keep database connections low and stable.
- This matters a lot in serverless or highly concurrent apps.
Cache read-heavy data
If many users request the same data:
- Use Redis or in-memory caching
- Cache expensive aggregates, sessions, feature flags, frequently read profiles, etc.
Reduce chatty access patterns
- Batch queries
- Use joins or CTEs where appropriate
- Fetch only needed columns
- Avoid doing one query per row
3) Add read replicas when reads dominate
If most traffic is reads and the primary is getting overloaded:
- Add read replicas
- Send read-only traffic to replicas
- Keep writes on the primary
This helps when:
- dashboards/query endpoints are heavy
- read traffic scales much faster than writes
Caveats:
- replicas are eventually consistent
- some queries need read-after-write consistency and must go to primary
- replication lag must be monitored
4) Partition large tables
When tables become very large, partitioning can help with:
- faster queries on time/range-based data
- easier retention/purging
- smaller indexes per partition
Good candidates:
- events
- logs
- audit records
- time-series or append-only tables
Typical strategies:
- range partitioning by date
- hash partitioning by tenant/user if you have a multi-tenant workload
Don’t partition too early—only when the table size and query patterns justify it.
5) Separate workloads
As your app grows, different query types can interfere with each other.
Examples:
- OLTP app traffic vs. analytics/reporting
- user-facing requests vs. background jobs
- hot operational data vs. historical data
Ways to separate:
- move analytics to a warehouse/OLAP system
- create replicas dedicated to reporting
- use different tables or schemas for different workloads
6) Plan for vertical and horizontal growth
Vertical scaling
The simplest next step:
- bigger CPU
- more RAM
- faster storage (NVMe)
- better IOPS
PostgreSQL often scales very well vertically before you need anything more complex.
Horizontal scaling
Harder with PostgreSQL, because writes generally still funnel to one primary. Options include:
- read replicas
- partitioning/sharding
- application-level sharding by tenant/user
- distributed Postgres variants if your use case fits
Sharding is usually a later-stage move because it adds operational complexity.
7) Monitor the right things
You can’t scale what you can’t see.
Track:
- CPU
- RAM / cache hit ratio
- disk I/O and latency
- active connections
- slow queries
- replication lag
- autovacuum behavior
- table/index bloat
- lock waits
Useful tools:
pg_stat_statementsEXPLAIN ANALYZEpg_stat_activitypgBadger- cloud provider metrics
- APM tools like Datadog, New Relic, or OpenTelemetry-based tooling
8) Keep vacuum and bloat under control
PostgreSQL uses MVCC, so updates/deletes create dead tuples. If not managed well:
- tables and indexes bloat
- queries slow down
- disk usage grows
Make sure:
- autovacuum is running effectively
- tables with heavy churn are monitored
- you aren’t creating long-lived transactions
9) Have a growth path in mind
A practical progression is:
- Single Postgres instance
- Connection pooling + indexing + query tuning
- Vertical scale up
- Read replicas
- Partition large tables
- Separate analytics/background workloads
- Sharding if absolutely necessary
Rule of thumb
If you’re only at a few thousand users, PostgreSQL should usually be fine on a single well-tuned instance. Most early scaling pain comes from:
- inefficient queries
- too many connections
- missing indexes
- no caching
- poor transaction habits
If you want, I can also give you:
- a Postgres scaling checklist for startups, or
- a decision tree for when to add replicas, partitioning, or sharding.