Scaling Up: Strategies for Ensuring Your Website Can Handle Growth
Scaling for sustained growth is a capacity and architecture problem: stateless horizontal scaling, database read replicas, a CDN strategy that holds up at higher steady-state volume, and load tests run before traffic forces the issue. That's different from surviving a flash sale or a viral spike, which is a queueing and degradation problem covered in our companion piece on sudden traffic spikes.
Sustained growth versus a spike
A growing business adds users steadily over months. Capacity planning for that curve is about right-sizing infrastructure ahead of a trend you can see coming, load testing to confirm headroom, and building an architecture that adds capacity linearly as demand grows.
A spike is a different shape entirely: a five-minute burst from a flash sale, a viral post, or a Black Friday drop. That needs queueing, cache stampede protection, and graceful degradation, which we cover in detail in the sibling post on handling sudden traffic spikes. Conflating the two leads teams to over-provision for spikes they rarely see, while under-investing in the database and caching work that sustained growth actually requires.
Vertical scaling has a ceiling horizontal scaling doesn't
Vertical scaling, moving to a bigger single server, is the fastest first fix and the wrong long-term strategy. It has a hard ceiling set by the largest instance type available, a single point of failure, and no relief if that one machine goes down. Horizontal scaling trades that simplicity for elasticity: more, smaller instances that can be added or removed independently, with no single instance being a failure domain for the whole app tier.
| Dimension | Vertical scaling | Horizontal scaling |
|---|---|---|
| Implementation effort | Low, resize an existing instance | Higher, requires statelessness and a load balancer |
| Ceiling | Hard limit at the largest available instance size | Effectively open-ended |
| Failure domain | Single instance; its failure takes the app down | Distributed; one instance failing degrades capacity, not availability |
| Cost curve | Non-linear, larger instances cost disproportionately more | Roughly linear with instance count |
Horizontal scaling: the app tier
Elastic Load Balancing distributes incoming traffic across multiple targets, running health checks on a configurable cadence and routing only to healthy instances (AWS — What is Elastic Load Balancing?). That's the mechanism that lets you add app instances as load grows instead of buying a bigger single server.
Growth-stage architecture: a stateless, horizontally scaled app tier sitting between a CDN and a data layer split into cache, a write-only primary, and read replicas.
Statelessness is the actual requirement
Adding instances behind a load balancer only works if any instance can serve any request. Session data, uploaded files, and in-memory caches need to live outside the app process, in a shared cache or object store, or the load balancer starts routing users to the one instance that happens to hold their state.
Sticky sessions, where a load balancer pins a user to one instance for the life of their session, look like a quick fix for this. They're an anti-pattern for growth: they defeat the point of horizontal scaling by concentrating load unevenly, and they turn a single instance restart into a mass logout for everyone pinned to it.
More app instances means more database connections, not fewer problems
Every new app instance typically opens its own pool of database connections. Doubling instance count without revisiting pool size can exhaust the database's connection limit well before CPU or memory becomes the constraint, a failure mode that looks like a database problem but is actually an app-tier scaling side effect. A connection pooler sitting in front of the database, decoupling the number of app-side connections from the number the database actually has to manage, is the standard fix once instance count grows past a handful.
Horizontal scaling isn't "add more servers." It's "make sure any server can answer any request," which is a statelessness requirement most teams don't confront until the second or third instance goes live.
Database scaling: read replicas
Amazon RDS read replicas serve high-volume read traffic by replicating a primary database asynchronously; the primary handles writes while replicas absorb read-only queries (AWS — Working with DB instance read replicas). PostgreSQL's own streaming replication does the same thing at the database engine level, independent of any specific cloud vendor (PostgreSQL Documentation — Log-Shipping Standby Servers).
The pattern only helps if your application's query pattern is actually read-heavy, and if your code can route read queries to replicas while keeping writes on the primary. Retrofitting that routing logic into an application that assumes a single database connection is real engineering work, not a configuration flag.
A read replica is not a performance switch you flip. It only pays off once your application can actually route reads and writes to different connections, which is a code change, not an infrastructure change.
What replicas don't fix
Replication is asynchronous, so a replica can lag behind the primary by a small, variable amount. Any code path that writes data and immediately reads it back, an order confirmation page, for example, needs to read from the primary or handle a stale read explicitly. Read replicas also don't help write throughput at all; that's a sharding or partitioning problem, a different and harder one.
Backups aren't the same thing as replicas, and growth makes the distinction matter
A read replica gives you read scale and, in a failover, a promotable standby. It isn't a substitute for point-in-time backups, since a replica faithfully replicates a bad write or a corrupted row just as fast as a good one. As data volume grows, verify backup and restore procedures against a realistic dataset size on a recurring schedule, not only once at initial setup.
CDN strategy at steady-state scale
At growth-stage traffic volumes, a CDN's job shifts from "handle bursts" to "keep origin load flat as baseline traffic climbs." Tiered caching architectures route a cache miss through an upper-tier data center before it reaches the origin, which concentrates origin connections into a small, predictable set instead of every edge location hitting the origin independently (Cloudflare — Tiered Cache).
That matters more as your user base grows and spreads geographically. More edge locations serving more regions means more potential origin connections if tiering isn't configured, even before accounting for a single traffic spike.
Cache hit ratio is the metric that tells you if the CDN is actually working
A CDN that's misconfigured, wrong cache headers, overly short TTLs, cache keys that vary unnecessarily by query parameter, quietly forwards most requests to origin anyway while still billing for edge traffic. Tracking cache hit ratio over time, and investigating any downward trend as your catalog or traffic grows, catches this before it shows up as unexplained origin load during a normal week, let alone a busy one.
Load testing before growth forces the issue
k6 is an open-source load testing tool that runs scripted virtual users against your application and reports latency, error rate, and throughput under load (Grafana k6 documentation). The tool defines several distinct test types, each answering a different capacity question (Grafana k6 — API load testing):
| Test type | Question it answers | When to run it |
|---|---|---|
| Smoke test | Does the system function at all under minimal load? | Every deploy, as a sanity check |
| Load test | How does the system perform under expected, normal traffic? | Before and after infrastructure changes |
| Stress test | How does performance degrade beyond normal peak? | Quarterly, ahead of known high-traffic periods |
| Breakpoint test | Where exactly does the system fail as load ramps up? | Once per major architecture change |
| Soak test | Does performance degrade over hours under sustained load? | Before launches expected to run at elevated traffic for days |
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 50 },
{ duration: '5m', target: 50 },
{ duration: '2m', target: 0 },
],
};
export default function () {
http.get('https://staging.example.com/product/123');
sleep(1);
}
That script ramps to 50 virtual users, holds for 5 minutes, and ramps down, a basic load test shape. Point it at staging with production-representative data volumes, not an empty database, or the results won't reflect what actually happens once real growth arrives.
Read the tail latency, not only the average
An average response time hides the users having the worst experience. A load test that reports p50, p95, and p99 latency separately shows whether 1 request in 100 is timing out even while the median looks healthy, which is exactly the pattern a growing, unevenly loaded system tends to produce. Chasing the average down while ignoring a growing tail is a common way teams declare a system scaled when a meaningful slice of real users are still having a bad time.
What to add at each growth stage
- Early stage: a single app server plus managed database is usually fine. Add a CDN for static assets early; it's cheap and removes a whole class of later work.
- Second stage: move to a stateless app tier behind a load balancer. This is also when session storage needs to move out of process memory.
- Third stage: add read replicas once read query volume is measurably the bottleneck, not before. Premature replication adds operational complexity without payoff.
- Fourth stage: run scheduled load and stress tests against staging, sized to your actual growth trajectory, not a guess.
- Confirm session and file storage are external to the app process before adding a second instance.
- Confirm your ORM or query layer can route reads to a replica pool distinct from the write connection.
- Confirm CDN tiered caching is enabled before traffic spreads across more edge regions.
- Run a load test against a realistic data volume before every major release, not only before an anticipated peak.
FAQ
How is scaling for growth different from handling a traffic spike?
Growth is a steady, predictable curve you architect for ahead of time: horizontal scaling, read replicas, CDN tiering. A spike is a short, sharp burst that needs queueing and graceful degradation, covered in our companion post.
When should I add database read replicas?
When read query volume is the measured bottleneck on your primary database, not preemptively. Replication adds real operational complexity, including handling replication lag in your application logic.
Do I need Kubernetes to scale horizontally?
No. A load balancer plus stateless app instances is the core requirement; container orchestration is one way to manage that fleet, not a prerequisite for horizontal scaling itself.
What's the minimum load test I should run before a growth milestone?
A load test at your expected peak concurrency, against staging with production-representative data, is the minimum. Add a stress test if you want to know the actual breaking point, not only whether the expected peak is safe.
Does a CDN replace the need for backend scaling work?
No. A CDN offloads static assets and cacheable HTML, which reduces origin load, but dynamic, personalized, or write-heavy requests still hit your app and database tier directly.
What's the most common scaling mistake as an app tier grows?
Not revisiting database connection pool sizing as instance count increases. Each new app instance typically opens its own connection pool, and that can exhaust a database's connection limit long before CPU or memory becomes the bottleneck.