Scaling Your Database Through Growing Pains

A practical guide to navigating database scaling challenges as your startup grows from hundreds to millions of users without costly rewrites

#startups#database#scaling#growing-pains
Cover image for the article: Scaling Your Database Through Growing Pains

Database scaling is the most common technical crisis in growing startups. You start with a single PostgreSQL instance that handles everything beautifully at 100 users. Then at 10,000 users, queries slow down. At 100,000 users, your database CPU pegs at 95% during peak hours. At a million users, you face a choice: throw money at bigger hardware or redesign your data architecture.

I have navigated this journey multiple times. The key insight is that database scaling is not a single decision — it is a sequence of interventions, each buying you time until the next growth threshold. The companies that handle it well plan ahead. The ones that struggle wait until everything is on fire.

The Scaling Journey

Every startup database goes through predictable phases:

PhaseUsersDB LoadTypical ProblemSolution Category
1. Single instance0-10KLowNoneEnjoy the simplicity
2. Query optimization10K-50KMediumSlow queriesIndexing, query tuning
3. Read scaling50K-200KHighRead contentionRead replicas, caching
4. Write scaling200K-1MVery HighWrite bottlenecksPartitioning, queue-based writes
5. Architectural scaling1M+CriticalFundamental limitsSharding, polyglot persistence

Chart

Phase 2: Query Optimization (First Wins)

Before adding infrastructure, optimize what you have. This phase typically buys you 3-6 months of headroom:

Index Strategy

The most impactful optimization is proper indexing. Most seed-stage databases are dramatically under-indexed:

  • Identify slow queries. Enable pg_stat_statements and monitor queries exceeding 100ms
  • Add composite indexes. For queries filtering on multiple columns, composite indexes outperform multiple single-column indexes
  • Cover your queries. Include columns in indexes that satisfy common SELECT lists (covering indexes)
  • Remove unused indexes. Each index slows writes. Monitor pg_stat_user_indexes for indexes with zero scans

Query Pattern Fixes

Anti-PatternProblemFix
SELECT *Fetches unnecessary dataSelect only needed columns
N+1 queriesDatabase round trips per itemEager loading, batch queries
Missing LIMITFull table scans on listsAlways paginate
Unfiltered JOINsCartesian productsAdd WHERE clauses, verify join conditions
Functions in WHEREPrevents index usagePrecompute or use expression indexes

Connection Management

Connection exhaustion is a common early scaling problem. Solutions:

  • Use connection pooling (PgBouncer for PostgreSQL)
  • Set appropriate pool sizes per service
  • Implement connection timeout and retry logic
  • Monitor active connections versus pool capacity

Phase 3: Read Scaling

When query optimization reaches its limits, separate read and write traffic:

Read Replicas

Add asynchronous read replicas for read-heavy workloads:

  • Route reporting and analytics queries to replicas
  • Route dashboard and list views to replicas
  • Keep writes and consistency-critical reads on the primary
  • Monitor replication lag and handle stale reads gracefully

Caching Layers

Implement caching strategically:

Cache LevelWhat to CacheTTL StrategyInvalidation
ApplicationComputed values, API responses1-60 minutesWrite-through
Query resultExpensive query results5-30 minutesTime-based or event-based
ObjectFrequently accessed entities1-24 hoursWrite-through
CDN/EdgeStatic content, public API responses1-24 hoursPurge on deploy

Cache invalidation rules:

  1. Start with time-based expiration (simple, predictable)
  2. Add event-based invalidation only for data where staleness is user-visible
  3. Never cache user-specific data in shared caches without proper isolation
  4. Monitor cache hit rates — anything below 80% needs investigation

Materialized Views

For complex aggregations and dashboards, materialized views trade write-time computation for read-time speed:

  • Create materialized views for dashboard metrics
  • Refresh on a schedule matching your staleness tolerance
  • Index materialized views like regular tables
  • Monitor refresh times as data volume grows

Phase 4: Write Scaling

Write scaling is harder than read scaling because you cannot simply add replicas. Strategies:

Table Partitioning

Partition large tables by time or tenant:

  • Time-based partitioning for event/log tables — enables fast range queries and efficient archival
  • Tenant-based partitioning for multi-tenant SaaS — enables per-tenant performance isolation
  • Hash partitioning for even distribution across partitions

Write Buffering

Decouple writes from user requests:

  • Queue non-critical writes (analytics events, audit logs) through message queues
  • Batch bulk operations during off-peak hours
  • Implement write-ahead patterns for eventual consistency where acceptable

Schema Design for Write Performance

Design ChoiceWrite ImpactTrade-off
Fewer indexesFaster writesSlower reads on unindexed columns
Append-only tablesVery fast insertsComplex updates/deletes
JSONB for flexible dataFaster writes (no ALTER TABLE)Slower queries on nested fields
DenormalizationFewer writes per transactionData consistency complexity

Phase 5: Architectural Scaling

When single-database optimizations reach their limits:

Horizontal Sharding

Distribute data across multiple database instances:

  • Tenant-based sharding — Each tenant's data lives on a specific shard. Simple routing, tenant-isolated performance.
  • Hash-based sharding — Distribute rows across shards by hashing a key. Even distribution, complex cross-shard queries.
  • Geography-based sharding — Data lives close to users. Good for latency, complex for global queries.

Polyglot Persistence

Use different databases for different access patterns:

Access PatternOptimal DatabaseExample Use Case
Relational/transactionalPostgreSQLUser accounts, billing
High-write event streamsClickHouse, TimescaleDBAnalytics, audit logs
Full-text searchElasticsearch, MeilisearchProduct search, content
Key-value/sessionRedis, DragonflyDBCaching, sessions, real-time
Document/flexible schemaMongoDBContent management, configs
Graph relationshipsNeo4jSocial graphs, recommendations

Decision Framework: When to Act

Do not scale preemptively. Scale when metrics indicate necessity:

MetricInvestigateAct NowEmergency
Query p95 latency> 200ms> 500ms> 2000ms
CPU utilization> 60% sustained> 80% sustained> 90% sustained
Connection pool usage> 60%> 80%> 95%
Disk IOPS> 60% of provisioned> 80% of provisionedHitting limits
Replication lag> 1 second> 5 seconds> 30 seconds

Common Mistakes

Sharding too early. Sharding adds enormous complexity. Most startups never need it. Exhaust vertical scaling, read replicas, and caching first.

Ignoring query patterns. Adding hardware without understanding why queries are slow wastes money. Profile before you scale.

Caching everything. Caching without invalidation strategy creates subtle, hard-to-debug data consistency issues. Cache deliberately.

Choosing the wrong database for growth. If you started with a database that does not support read replicas or partitioning, migration becomes mandatory rather than optional.

Key Takeaways

  • Database scaling is a sequence of interventions, not a single architectural decision — each phase buys time until the next growth threshold
  • Start with query optimization and indexing before adding infrastructure — this alone typically provides 3-6 months of headroom
  • Separate read and write traffic using read replicas and strategic caching before considering more complex approaches
  • Implement caching with explicit invalidation strategies — time-based first, event-based only where staleness is user-visible
  • Scale based on metrics (p95 latency, CPU utilization, connection pool usage), not predictions or anxiety
  • Sharding is a last resort for most startups — exhaust vertical scaling, replicas, partitioning, and caching first
  • Document your scaling decisions and thresholds so future engineers understand not just what was done but why

Database scaling is not a problem you solve once. It is a continuous practice of monitoring, anticipating, and intervening at the right time. The companies that do this well build monitoring before they need it, plan interventions before they are urgent, and document decisions so the next engineer can continue the journey.

Comments

    No comments yet. Be the first to share your thoughts.