Your application is fast. Your API responds in milliseconds. But your database queries are taking seconds. Page loads are getting slower. Timeouts are appearing in your error logs. Your database has become the bottleneck, and adding more application servers will not help.
Here are five strategies that actually fix database performance problems at startup scale.
Strategy 1: Query optimization (free, immediate impact)
Before throwing hardware at the problem, look at your queries. In our experience, 80% of database performance problems are caused by 5% of the queries. Find them and fix them.
Turn on slow query logging. In PostgreSQL, set log_min_duration_statement to 100 milliseconds. This logs every query that takes longer than 100ms. Run this for a week and analyze the results.
The usual suspects:
- Missing indexes: A query that scans 10 million rows to find 10 results because there is no index on the WHERE clause columns. Adding the right index can turn a 5-second query into a 5-millisecond query.
- N+1 queries: Loading a list of 100 items, then running a separate query for each item is related data. This turns one query into 101 queries. Fix with JOIN or batch loading.
- Unbounded queries: SELECT * FROM large_table with no LIMIT. Always paginate large result sets.
- Missing EXPLAIN ANALYZE: Run EXPLAIN ANALYZE on your slow queries. It tells you exactly where time is spent: sequential scans, sort operations, hash joins. The output looks intimidating but becomes readable with practice.
Strategy 2: Connection pooling ($0-$50/month)
Every database connection consumes memory on the database server. PostgreSQL allocates roughly 10MB per connection. If your application opens 200 connections, that is 2GB of RAM just for connection overhead.
Use PgBouncer as a connection pooler between your application and the database. PgBouncer maintains a pool of database connections and multiplexes application connections onto them. 200 application connections might only need 20 actual database connections.
On AWS, RDS Proxy provides this as a managed service. Cost: $0.015 per vCPU per hour. For a db.r5.large (2 vCPUs), that is about $22/month. Worth it for the connection management alone.
Strategy 3: Read replicas ($100-$500/month)
Most SaaS applications are read-heavy: 80-90% of database queries are reads. Adding a read replica lets you direct read traffic to a separate database instance while writes go to the primary.
Implementation is straightforward with most ORMs: configure a separate database connection for reads and use it for queries that do not need real-time consistency. Dashboard queries, analytics, search, and reporting can all be served from the replica.
Caveats: read replicas have replication lag (typically 10-100ms on RDS). Any query that needs to see the most recent data must go to the primary. For most features, 100ms of lag is imperceptible.
Strategy 4: Caching layer ($50-$200/month)
Redis or Memcached as a caching layer can eliminate the most expensive and frequent database queries entirely. Cache results that are expensive to compute and change infrequently: user permissions, configuration data, aggregated metrics, and API responses.
A simple caching pattern: before running a database query, check if the result is in Redis. If yes, return the cached result. If no, run the query, store the result in Redis with a TTL (time to live), and return the result.
Cache invalidation is the hard part. Start with TTL-based expiration (cache for 60 seconds, 5 minutes, 1 hour depending on how stale the data can be). Add explicit invalidation later when needed (clear the cache when the underlying data changes).
Strategy 5: Database partitioning and sharding
If your database is genuinely too large for a single instance (100GB+ of active data, billions of rows in a single table), you need to partition or shard.
Partitioning (single database, split tables): PostgreSQL native partitioning lets you split a large table into smaller partitions based on a key (date, tenant ID, geographic region). Queries that filter by the partition key only scan the relevant partition, dramatically reducing query time.
Sharding (multiple databases): Distribute data across multiple database instances based on a shard key (usually tenant ID for multi-tenant SaaS). Each shard holds a subset of tenants. This provides near-linear horizontal scaling but adds significant application complexity.
Sharding is a last resort. Before sharding, exhaust strategies 1-4. Most startups never need to shard their database. Companies like Basecamp serve millions of users from a single PostgreSQL database. The threshold for sharding is much higher than most engineers think.
Database performance issues?
traztech helps startups diagnose and fix database performance bottlenecks. From query optimization to architecture redesign, we get your database running fast without over-engineering the solution.
Book a free strategy call