Scalability and Availability for Backend Systems
A staff-engineer guide to horizontal scaling, stateless services, L4/L7 load balancing, redundancy, and defining availability targets.
Scalability and Availability
Every high-level design interview and every real production incident eventually comes back to the same two questions: can this system handle more load, and does it keep working when parts of it fail? Scalability and availability are not features you bolt on later — they are architectural decisions baked in from the first component diagram. This guide covers the vocabulary and mechanics a backend engineer with 2-3 years of experience needs to reason about both, and to defend design choices under interview-style questioning.
1. Vertical vs Horizontal Scaling
Vertical scaling (scale up) means adding more resources — CPU, RAM, faster disks — to a single machine. Horizontal scaling (scale out) means adding more machines and distributing load across them. Almost every modern backend system is designed to scale horizontally, because vertical scaling has a hard ceiling and a single point of failure.
Trade-off comparison
| Dimension | Vertical scaling | Horizontal scaling |
|---|---|---|
| Ceiling | Hard limit (largest instance type available) | Effectively unbounded (add more nodes) |
| Downtime to scale | Usually requires a restart/reboot | Zero — new nodes join the pool live |
| Fault tolerance | Single point of failure | Node failure is tolerable, traffic reroutes |
| Cost curve | Superlinear — bigger instances cost disproportionately more | Roughly linear per unit of capacity |
| Complexity | Low — no distributed state to manage | Higher — needs statelessness, load balancing, coordination |
| Good for | Databases with strong consistency needs, legacy monoliths, quick wins | Stateless API tiers, web servers, worker pools |
These aren't mutually exclusive. Most production systems scale horizontally at the application tier and vertically (plus read replicas or sharding) at the data tier, because databases are far harder to scale out than stateless services. When you're asked "how would you scale this?" in an interview, the strongest answer separates the stateless tier (scale horizontally, trivially) from the stateful tier (scale vertically first, then replicate/shard).
Diminishing returns and Amdahl's Law
Horizontal scaling isn't free linear throughput either. If a request has to touch a shared, non-parallelizable resource — a single-writer database, a global lock, a serial log — that portion caps your speedup no matter how many nodes you add. Amdahl's Law formalizes this: if a fraction p of the work is parallelizable and (1 - p) is serial, the maximum speedup with N nodes is:
speedup(N) = 1 / ((1 - p) + p / N)
If 10% of your request path is a serialized write to a single database (p = 0.9), your theoretical maximum speedup caps at 10x, no matter how many app servers you add. This is why "just add more instances" stops working once you hit the database — the real scaling work moves to caching, read replicas, sharding, or async processing.
2. Stateless Service Design
Horizontal scaling only works cleanly if any request can be served by any node. That requires the service tier to be stateless — no request-specific data lives in the memory or local disk of a particular instance between requests.
Where state tries to sneak in
| Anti-pattern | Why it breaks horizontal scaling | Fix |
|---|---|---|
| In-memory HTTP session | Session lost if next request lands on a different node | Externalize to Redis, or use signed stateless JWTs |
| Local file uploads | File only exists on the node that received it | Upload directly to object storage (S3/GCS), not local disk |
| In-process caches with write-through | Cache diverges between nodes, stale reads | Shared cache (Redis) or cache invalidation via pub/sub |
| Sticky sessions at the load balancer | Defeats even distribution, hot nodes accumulate long-lived connections | Prefer stateless auth so any node can serve any request |
| In-memory rate limiting / counters | Each node undercounts, allows N × limit through | Centralized counter (Redis INCR + TTL) or a rate-limiting service |
| Local cron/scheduled jobs | Job runs once per node instead of once total | Use a leader-elected scheduler or an external job runner |
Sticky sessions are a scaling smell. They're sometimes used as a stopgap when a service has accidental in-memory state, but they concentrate load unevenly, make deploys and autoscaling painful (draining a node with 10,000 pinned sessions is slow), and reintroduce a single point of failure per user. Prefer fixing the statefulness at the source — externalize the state — over papering over it with routing tricks.
Statelessness is also what makes autoscaling safe: a new instance can join the pool and start serving traffic immediately with zero warm-up beyond process startup, and an instance can be terminated at any time without losing in-flight user context (as long as in-flight requests are drained, which is a separate concern — see failover below).
3. Load Balancers: L4 vs L7
A load balancer distributes incoming traffic across a pool of backend instances. The critical distinction for interviews is which OSI layer it operates at, because that determines what it can see and what decisions it can make.
L4 vs L7 comparison
| Aspect | L4 (Transport) | L7 (Application) |
|---|---|---|
| Operates on | IP address + TCP/UDP port | Full HTTP request (path, headers, cookies, method) |
| Routing decisions | Which server gets this connection | Path-based, header-based, host-based routing |
| Performance | Very fast — no payload inspection | Slower — must terminate/parse the request |
| TLS termination | Usually passes through (or terminates at TCP level) | Commonly terminates TLS to inspect the request |
| Examples | AWS NLB, LVS, HAProxy (TCP mode), IPVS | AWS ALB, NGINX, Envoy, HAProxy (HTTP mode) |
| Use when | Raw throughput matters, non-HTTP protocols (gRPC over raw TCP, databases) | Content-based routing, A/B testing, canary releases, WAF rules |
| Sticky sessions | Based on IP/connection | Based on cookies (application-aware) |
Interview shorthand: L4 load balances connections, L7 load balances requests. A single long-lived HTTP/2 or gRPC connection can carry many logical requests — an L4 balancer sends them all to the same backend, while an L7 balancer can distribute individual requests across backends even within the same connection. This matters a lot for gRPC services behind a plain TCP load balancer, which is a classic "why is my traffic unbalanced" production bug.
A minimal L7 config, for concreteness
upstream orders_service {
least_conn; # route to the backend with fewest active connections
server 10.0.1.10:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.11:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.12:8080 max_fails=3 fail_timeout=30s;
keepalive 64; # reuse connections to upstreams
}
server {
listen 443 ssl;
server_name api.example.com;
location /orders/ {
proxy_pass http://orders_service;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 2s;
proxy_read_timeout 5s;
proxy_next_upstream error timeout http_502 http_503; # retry on next node
}
}Load balancing algorithms
| Algorithm | Behavior | Best for |
|---|---|---|
| Round robin | Cycles through servers in order | Uniform request cost, homogeneous servers |
| Least connections | Sends to the server with fewest active connections | Variable request duration (e.g., some requests slow, some fast) |
| Weighted round robin | Round robin with per-server weights | Heterogeneous instance sizes |
| IP hash / consistent hash | Same client (or key) always routes to the same backend | Session affinity, cache locality |
| Least response time | Combines connection count with observed latency | Latency-sensitive services with variable backend health |
4. Reverse Proxies vs Load Balancers vs API Gateways
These three terms get used interchangeably in casual conversation but have distinct primary responsibilities, even though a single piece of software (NGINX, Envoy) often plays more than one role at once.
| Component | Primary job | Sits where | Typical extras |
|---|---|---|---|
| Reverse proxy | Sits in front of one or more origin servers, forwards client requests to them | Edge of a service or a whole site | TLS termination, static asset caching, compression, request buffering |
| Load balancer | Distributes traffic across a pool of identical backend instances | Between a proxy/gateway and a service's instances | Health checks, connection draining, weighted routing |
| API gateway | Single entry point for many different backend services, applies cross-cutting policy | Edge of the whole system, in front of many services | AuthN/AuthZ, rate limiting, request/response transformation, aggregation, API versioning |
In a microservices system these three responsibilities are usually layered: an API gateway (Kong, AWS API Gateway, Envoy-based) handles auth and routing to the right service, and each service sits behind its own load balancer distributing across its replica pool. In a smaller monolithic deployment, a single NGINX instance often plays all three roles at once — that's fine, but be able to name which responsibility a config block is serving when asked.
5. Redundancy and Failover
Redundancy means running more capacity than the minimum needed, specifically so that the failure of any single component doesn't take down the system. Failover is the mechanism that detects a failure and redirects traffic away from it.
Levels of redundancy
| Level | What's duplicated | Protects against |
|---|---|---|
| Instance-level | Multiple app server replicas behind a load balancer | Single process crash, OOM, deploy issue |
| Zone-level (AZ) | Instances spread across availability zones | A single data center losing power/network |
| Region-level | Full stack deployed in multiple geographic regions | Regional outage, natural disaster |
| Data-level | Database replicas (sync or async) | Primary DB node failure, disk corruption |
| Dependency-level | Fallback for a third-party API (cache, degraded mode) | Upstream vendor outage |
Redundancy without health checks is theater. A load balancer with three backends is not actually redundant if it keeps sending traffic to a dead instance for 60 seconds before noticing. The health check interval and failure threshold directly determine your effective MTTR (mean time to recovery) for a single-node failure — tune them deliberately (e.g., 3 failures at 5s intervals = ~15s detection), and make sure the check hits a real dependency chain (/health should verify DB connectivity, not just "process is up").
Active-passive vs active-active
| Model | Description | Failover time | Cost | Complexity |
|---|---|---|---|---|
| Active-passive | Standby replica is idle until primary fails | Seconds to minutes (needs promotion) | Lower — standby is underutilized | Simpler — no split-brain risk if done right |
| Active-active | All replicas serve live traffic simultaneously | Near-zero — traffic already flowing elsewhere | Higher — all nodes fully provisioned | Harder — needs conflict resolution / partitioning for writes |
Most real systems mix both: active-active for reads (any replica can serve a read), active-passive for writes (single writer, promoted on failure). This is exactly the shape of a typical primary/replica relational database setup, and it's a strong default answer when an interviewer asks "how do you make the database highly available?"
Graceful degradation over hard failure
Redundancy handles component loss; graceful degradation handles overload or partial dependency failure without a full outage:
- Circuit breakers — stop calling a failing downstream dependency after an error threshold, fail fast instead of piling up threads waiting on timeouts.
- Bulkheads — isolate resource pools (thread pools, connection pools) per dependency so one slow dependency can't starve the whole service.
- Load shedding — reject low-priority requests first when the system is over capacity, to protect the requests that matter.
- Fallback responses — serve stale cached data or a reduced feature set rather than an error page (e.g., show cached recommendations if the personalization service is down).
6. Defining and Measuring Availability
"Highly available" is meaningless without a number attached. Availability is conventionally expressed as a percentage of uptime over a period, and colloquially referred to by its "nines."
The nines table
| Availability | Downtime / year | Downtime / month | Downtime / week |
|---|---|---|---|
| 99% ("two nines") | 3.65 days | 7.2 hours | 1.68 hours |
| 99.9% ("three nines") | 8.76 hours | 43.2 minutes | 10.1 minutes |
| 99.95% | 4.38 hours | 21.6 minutes | 5 minutes |
| 99.99% ("four nines") | 52.6 minutes | 4.32 minutes | 1.01 minutes |
| 99.999% ("five nines") | 5.26 minutes | 25.9 seconds | 6.05 seconds |
Five nines is extraordinarily expensive and rarely the right target. It implies your entire stack — including every third-party dependency, DNS, your cloud provider's control plane, and your deploy process — never causes more than ~5 minutes of cumulative downtime in a year. Most consumer-facing products target 99.9%–99.95%; core payment or infrastructure paths might justify 99.99%. Always ask "what does the business actually lose per minute of downtime?" before committing to a nines target — it should come from a cost/risk conversation, not be picked because it sounds impressive.
SLI, SLO, SLA, and error budgets
| Term | Definition | Example |
|---|---|---|
| SLI (Indicator) | A measured metric of service behavior | "% of requests completed in < 300ms", "% of requests returning non-5xx" |
| SLO (Objective) | An internal target for an SLI | "99.9% of requests succeed over a rolling 30 days" |
| SLA (Agreement) | An external, often contractual, commitment — usually looser than the internal SLO | "99.5% uptime or customer receives service credits" |
| Error budget | 1 - SLO, the amount of unreliability you're allowed to spend | 99.9% SLO → 0.1% error budget → ~43 min/month you can "spend" on deploys, experiments, risk |
The error budget is a planning tool, not just a report card. If you've burned your error budget for the month, the standard practice is to freeze risky changes (new feature launches, risky migrations) and prioritize reliability work until the budget resets. If you're well under budget, that's a signal you can afford to ship faster and take more risk. This reframes "reliability vs velocity" from a philosophical debate into a number both engineering and product can agree on.
Availability of a composed system
When a request depends on multiple components in series, availabilities multiply — this is a common interview gotcha.
Overall availability = A1 × A2 × A3 × ... × An (for components in series)
For example, a request that must succeed through an API gateway (99.99%), an app service (99.95%), and a database (99.95%):
0.9999 × 0.9995 × 0.9995 ≈ 0.9989 → 99.89% overall, NOT 99.95%
Chaining "highly available" components in series reduces overall availability. This is exactly why redundancy at each hop (so each hop's effective availability approaches 100% via failover) matters more as the request path gets longer — and why deeply nested microservice call chains are an availability risk in their own right, independent of any single service's reliability.
For components that are genuinely redundant in parallel (e.g., either replica can serve the request), availability improves:
Availability(parallel) = 1 - [(1 - A1) × (1 - A2)]
Two independent nodes each at 99% availability, deployed in parallel with automatic failover, together achieve 1 - (0.01 × 0.01) = 99.99% — this is the mathematical justification for "just add a standby."
7. Back-of-Envelope Capacity Planning
Scalability decisions should be grounded in numbers, not vibes. A standard capacity estimate for an interview or a real capacity-planning doc:
Given:
- 10 million daily active users (DAU)
- Each user makes ~20 requests/day on average
- Traffic is not uniform: peak = 3x average
Average requests/day = 10,000,000 × 20 = 200,000,000
Average requests/sec = 200,000,000 / 86,400 ≈ 2,315 req/s
Peak requests/sec = 2,315 × 3 ≈ 6,945 req/s
If each app server instance safely handles 500 req/s at target latency:
Instances needed at peak = 6,945 / 500 ≈ 14 instances
Add N+2 redundancy for zone loss / rolling deploys → provision ~16-18 instances
Interviewers care less about the exact numbers and more about whether you (1) state your assumptions explicitly, (2) account for peak-vs-average skew, and (3) connect the estimate back to a concrete decision — like instance count, cache size, or database read-replica count. A capacity estimate that doesn't change any design decision wasn't worth doing.
8. Production Observations
- Autoscaling reacts to metrics, it doesn't predict them. Scale-out based on CPU or request-rate has a lag (metric collection interval + new instance boot time + warm-up). For predictable traffic spikes (e.g., a sale event at a known time), schedule pre-scaling ahead of the event rather than relying purely on reactive autoscaling.
- Connection pool exhaustion, not CPU, is the most common real-world scaling bottleneck. Doubling app server instances without raising the database's max connection limit (or adding a connection pooler like PgBouncer) just moves the bottleneck to
too many connectionserrors at the database. - Health checks need to check the right thing. A
/healthendpoint that returns200 OKunconditionally provides zero signal. It should verify the dependencies the instance actually needs (DB reachable, cache reachable) without becoming so expensive it causes false failures under load. - Load balancer timeouts should be shorter than client timeouts, and backend timeouts shorter than load balancer timeouts. Otherwise the client gives up and retries while the backend is still working, doubling load during exactly the period the system is struggling.
- Zone-level redundancy is table stakes; region-level is a deliberate, costly decision. Multi-region active-active systems introduce data replication lag and conflict resolution complexity — don't reach for it unless the business case (regulatory residency, latency to a global user base, disaster recovery RTO) justifies it.
- Every "nines" target implies an operational cost — on-call rigor, deployment safety (canaries, automated rollback), and testing (chaos engineering, game days). Don't commit to 99.99% in a design doc without the operational muscle to back it.
Key takeaways
- Horizontal scaling is the default for stateless service tiers; vertical scaling and replication/sharding carry the data tier.
- Statelessness (externalized sessions, no local-disk dependency, centralized rate limits) is the prerequisite that makes horizontal scaling and autoscaling safe.
- L4 balances connections, L7 balances requests — know which one you're being asked about, since it changes what routing decisions are even possible.
- Redundancy only pays off if health checks and failover actually detect and route around failures fast enough to matter.
- Availability of components chained in series multiplies down; parallel redundancy multiplies failure probabilities down — always distinguish which topology you're computing for.
- SLOs and error budgets turn "how reliable should this be" into a number that governs both incident response and release velocity.
- Capacity numbers should always tie back to a decision (instance count, cache size, shard count) — an estimate that changes nothing wasn't worth doing.
Interview Questions
- What is the difference between vertical and horizontal scaling, and when would you choose one over the other?
- Why does horizontal scaling require the service layer to be stateless? Give three examples of hidden state that breaks it.
- Explain the difference between an L4 and an L7 load balancer. Which would you use for a gRPC service, and why does the connection-vs-request distinction matter there?
- What's the difference between a reverse proxy, a load balancer, and an API gateway?
- How would you design failover for a service with three replicas across two availability zones?
- What's the difference between active-active and active-passive redundancy? What are the trade-offs?
- Define SLI, SLO, SLA, and error budget. How does an error budget change how a team ships features?
- If Service A depends on Service B and Service C in series, and each is individually 99.9% available, what is the overall availability of a request through all three? What does that imply about deep microservice call chains?
- How would you estimate the number of application server instances needed to serve a given peak request rate?
- What is Amdahl's Law, and how does it explain why "just add more servers" eventually stops helping?
- Why is a naive
/healthendpoint that always returns 200 dangerous in production? - Describe the difference between graceful degradation and a circuit breaker. How do they work together under partial outage?
- What operational practices are required to responsibly claim a 99.99% availability target?
- How would you detect and mitigate connection pool exhaustion when scaling out an application tier?