Codetail

Article 2 of 15

Scalability Fundamentals

Vertical vs horizontal. When systems buckle and how to fix them.

22 min read

Vertical Scaling: Bigger Hardware

The simplest response to a struggling server is to make it bigger. More CPU cores, more RAM, faster SSDs. This is vertical scaling, scaling up. No code changes required. No distributed systems complexity. Just buy a larger instance.

Vertical scaling is like replacing a 2-lane road with an 8-lane highway. It's faster, but you're still building on the same piece of land, there are hard limits to how wide you can go.

When vertical scaling wins

Simple and fast

No application changes. Resize the instance, restart, done. The fastest path from 'slow' to 'fast'.

No distributed complexity

One server means no network partitions, no consensus, no data synchronization headaches.

Single-threaded workloads

Some databases (older MySQL configs) or runtimes (Python GIL) benefit more from faster CPUs than more cores.

Stateful applications

If your app stores state in memory (sessions, caches), vertical scaling avoids the complexity of sharing that state across servers.

The hard limits of vertical scaling

Vertical scaling has a ceiling. AWS's largest instance (u-24tb1.metal) has 448 vCPUs and 24 TB of RAM, but it costs ~$250/hour and there's exactly one size beyond it: nothing. More practically, the ceiling you hit first is usually cost, not physical limits.

ProblemWhy vertical scaling doesn't solve it
Single point of failureOne server equals one failure domain. It goes down, everything goes down. No amount of RAM prevents hardware failure.
Geographic latencyA 96-core server in Virginia is still 70ms from London. Physics doesn't care about your CPU count.
Cost efficiency at scaleDoubling CPU doesn't double throughput. After a point, more servers (horizontal) is cheaper than bigger servers.
Maintenance windowsRebooting a single giant server takes everything offline. Multiple smaller servers can be rolled one at a time.

Don't skip vertical scaling prematurely.Many startups jump to microservices and Kubernetes before they've proven product-market fit. A single well-tuned server can handle more than you think, Instagram served 30M users from 3 servers before their first major rewrite.

Horizontal Scaling: More Servers

Instead of one big server, use many smaller ones. Horizontal scaling, or scaling out, adds capacity by adding instances. Each server handles a fraction of the traffic. Together, they handle the whole.

Horizontal scaling is like building more lanes on more roads in parallel cities, rather than making one road infinitely wide. It trades single-point-of-failure risk for distributed complexity.

Horizontal scaling architecture
๐ŸŒIncoming traffic (1000 RPS)
โš–๏ธ Load Balancer
๐Ÿ–ฅ๏ธ Server 1 ~333 RPS
๐Ÿ–ฅ๏ธ Server 2 ~333 RPS
๐Ÿ–ฅ๏ธ Server 3 ~334 RPS
๐Ÿ—„๏ธ Shared Database (with read replicas)

The stateless requirement

Horizontal scaling only works when any server can handle any request. This requires your application servers to be stateless, they don't store anything in memory between requests. All state lives in shared, external systems.

Sessions
โœ—In-memory session store
โœ“Redis session store, any server can read any session
User data
โœ—Local file uploads
โœ“S3 or object storage, any server can access any file
Cache
โœ—Local in-process cache
โœ“Redis, shared cache visible to all servers
Config
โœ—Per-server config files
โœ“Environment variables or config service, consistent across all servers

Auto-scaling: elastic capacity

Cloud platforms can automatically add or remove servers based on load metrics. This is auto-scaling, you define the rules, the platform adjusts capacity. This means you pay for what you use and handle traffic spikes without manual intervention.

IF CPU > 70% for 2 min โ†’ Add 2 servers

Common for CPU-bound workloads (image processing, ML inference)

IF Request queue depth > 100 โ†’ Add 1 server

For async worker pools, keep queue shallow

IF Custom metric (orders/sec) โ†’ Scale predictively

Scale before the spike (e.g., before a product launch)

IF CPU < 20% for 10 min โ†’ Remove 1 server

Scale in to save cost during quiet periods

The database bottleneck: Horizontal scaling is straightforward for stateless app servers, but databases are harder. A single primary DB can become the bottleneck. Solutions: read replicas for read-heavy workloads, connection pooling (PgBouncer), sharding for write-heavy workloads, or switching to databases designed for distribution (Cassandra, DynamoDB).

Load Balancer Algorithms

A load balancer is only as good as its distribution algorithm. The right choice depends on whether your servers are homogeneous, whether requests have varying costs, and whether users need to be pinned to a specific server.

๐Ÿ”„Round Robin

Routes requests in sequence: Server 1, Server 2, Server 3, Server 1... Equal distribution by count, not by load. Simple and effective when requests have similar cost.

Best for: Homogeneous servers, similar request cost
Avoid when: Expensive requests (some servers get unlucky runs)
๐Ÿ“ŠLeast Connections

Routes to the server with the fewest active connections. Naturally adapts to servers handling slow requests, faster servers accept more connections as they complete work.

Best for: Variable request duration, long-lived connections
Avoid when: Very short requests (overhead of counting outweighs benefit)
๐Ÿ”‘IP Hash

Hashes the client's IP to consistently route them to the same server. A poor man's sticky sessions, no cookie required. Breaks if server count changes (consistent hashing fixes this).

Best for: Stateful apps needing session affinity
Avoid when: Uneven traffic (hot IPs = hot servers)
โš–๏ธWeighted Round Robin

Like Round Robin, but servers with higher weight receive proportionally more requests. Useful when servers have different hardware specs, a 16-core server gets 2x the traffic of an 8-core one.

Best for: Heterogeneous server fleet
Avoid when: Dynamic environments where server capacity changes
๐ŸŽฒRandom

Picks a server at random. Surprisingly effective at scale, by the law of large numbers, distribution converges to even. Simpler to implement than Round Robin with similar results.

Best for: Simple setups, large server counts
Avoid when: Small server counts (statistical variance matters)

See it in action: the simulator

The simulator below models a round-robin load balancer with equal capacity servers. Drag the traffic slider to push the system toward overload, then add servers to restore health. Notice how response time degrades non-linearly as utilization approaches 100%.

Scalability Simulator
80 RPS
0 RPS500 RPS
High load, response times degrading (add capacity soon)
1 server ยท 100 RPS capacity
1
๐Ÿ–ฅ๏ธServer 1
50ms
80 RPS80% load

Queueing theory: At 50% utilization, response time โ‰ˆ 2ร— base. At 90%, it's 10ร—. At 99%, โˆž, the queue never drains.

Try pushing to 120 RPS on 1 server, then add servers to watch the response time recover.

The queueing model above (M/M/1) is a simplification, but the intuition is correct. Real systems degrade faster due to garbage collection pauses, memory pressure, and lock contention. Keep production utilization below 60-70% to maintain headroom for traffic spikes.

Real-World Scaling Patterns

Every high-scale system uses a combination of techniques rather than a single scaling strategy. Here are the patterns that appear repeatedly across large-scale architectures.

Database read replicas

Most applications read far more than they write (read:write ratio often 10:1 or higher). Horizontal scaling the app tier is easy, but the database becomes a chokepoint because every server hits the same primary. Read replicas solve the read side.

Read replica architecture
๐Ÿ—„๏ธ Primary DB
Writes only
replicates to
๐Ÿ—„๏ธ
Replica 1
Reads
๐Ÿ—„๏ธ
Replica 2
Reads
๐Ÿ—„๏ธ
Replica 3
Reads

App servers route writes to primary

App servers route reads to any replica

Replicas lag primary by milliseconds (async replication)

Replication lag trap:Replicas are eventually consistent with the primary. If a user writes a record and immediately reads it, they might hit a stale replica that hasn't received the update yet. For reads that must be fresh after a write, route to the primary or use "read-your-own-writes" consistency with a short primary bypass window.

The caching layer: skip the database entirely

The most effective scaling technique is not doing work at all. If a response can be served from cache, the database never sees the request. Redis or Memcached sitting between your app servers and database can absorb 90%+ of read traffic for read-heavy workloads.

Cache-aside

App checks cache first. Miss? Read from DB, write to cache. Most common pattern. App code is cache-aware.

General-purpose
Write-through

Every write goes to DB and cache simultaneously. Cache is always warm. Write latency doubles.

Read-heavy, tolerate write latency
Write-behind

Write to cache immediately, async-flush to DB. Fast writes, risk of data loss if cache node dies.

Write-heavy, can tolerate staleness

Sharding: horizontal database scaling

When a single database (even with replicas) can't handle write volume, you split data across multiple independent databases, each owning a shard of the keyspace.

Shard keyExampleRisk
User IDuser_id % 4 -> Shard 0-3Hot users (celebrities) create hot shards
Geographic regionEU -> Shard A, US -> Shard BUneven growth; US might outgrow EU 3x
Consistent hashhash(user_id) mod NComplex rebalancing when adding shards
Date range2024 data -> Shard C, 2025 -> Shard DRecent shards are always hottest (writes)

Sharding is a last resort. It adds massive complexity: cross-shard queries are expensive, transactions spanning shards require distributed coordination, and rebalancing is painful. Exhaust read replicas, connection pooling, caching, and vertical scaling before reaching for sharding. Most applications never need it.