Marcus Chen, YuSMP Group
Marcus Chen Staff Engineer, Backend & Cloud, YuSMP Group · Multi-tenant SaaS, AWS/GCP, distributed systems and database sharding

Web app scalability really comes down to one move: make every instance stateless, then scale it out behind a load balancer. From there, CDN and Redis caching soak up reads, message queues take slow work off the request path, and read replicas carry you a long way before sharding ever enters the picture. Autoscale on latency rather than CPU, and write SLOs so that an overloaded system bends into a degraded mode instead of snapping.

Horizontal vs vertical scaling: which should you choose?

Every production engineering team we support through web application development hits the same wall eventually: one server stops being enough. The first instinct is usually to buy a bigger machine, with more cores and more RAM. That is vertical scaling (scale-up), and there is nothing wrong with it as a starting move. A larger server needs no application changes and leaves your data layer untouched. For an early-stage product it can carry you surprisingly far — a modern 32-core, 128 GB RAM box will comfortably serve tens of thousands of concurrent users on a well-optimised monolith.

The trouble is the ceiling. Scale up to the largest instance a provider offers and you have run out of road — worse, your entire production workload now sits on a single point of failure. One hardware fault or availability-zone outage takes the whole thing down at once. That is why cloud-native teams treat vertical scaling as breathing room rather than a permanent answer.

Horizontal scaling (scale-out) spreads load across many identical instances. There is no theoretical ceiling: add instances as load climbs, retire them when traffic falls off. That elasticity is what modern web app scalability is built on. It comes with one hard precondition, though — your application has to run on many servers at once, each one oblivious to the state its siblings are holding. Which leads to the single most consequential decision in any scaling effort.

DimensionVertical scalingHorizontal scaling
CeilingHard (largest available instance)None in practice
Application changes requiredNoneStatelessness, shared data layer
Fault toleranceSingle point of failureRedundant — instances fail independently
Cost efficiency at scaleLow — large instances carry high premiumHigh — commodity instances, pay only for used capacity
Deployment complexityLowModerate (load balancer, service discovery)
Time to implementMinutes (instance resize)Days to weeks (architecture refactor)

Most teams end up combining the two: vertical scaling buys time while the horizontal work gets engineered. The mistake to avoid is treating the bigger box as a permanent answer. Resize when you need room to breathe, but kick off the stateless refactor in parallel, so you are never forced into it under production pressure.

This choice is independent of whether you run a monolith or microservices. As we cover in our guide to monolith vs microservices architecture, a well-structured monolith scales out every bit as well as a service mesh; what matters is statelessness, not how you package the deployment. Here we stay on the scaling techniques themselves and leave the decomposition question to that piece.

Stateless application design

Of every design principle behind horizontally scalable web applications, statelessness matters most. A stateless instance keeps no per-user or per-request data in memory between calls. Each incoming HTTP request carries its own context — usually a JWT or session token — so any available instance can handle it without side effects.

Compare that with a stateful app that keeps session data in memory on one particular server. Once a user logs in and their session lands on Server A, every later request has to come back to Server A — the pattern known as sticky sessions. That constraint quietly boxes in your options. Adding an instance leaves some users unable to find their session; removing one kills live sessions; and shipping a new version means a disruptive rolling restart that drops whatever was in flight. None of this is fatal at small scale, but it is a ceiling, and it arrives sooner than most teams expect.

The path to statelessness involves moving all mutable state out of application memory and into shared external stores:

  • Session data. Replace in-memory session objects with signed JWTs (for authentication state) or a distributed session store such as Redis. JWTs are self-contained and require no server-side storage; Redis sessions are centralised and can be invalidated server-side — both patterns allow any instance to handle any request.
  • User uploads and generated files. Never write files to the local disk of an application server. Store them in object storage (AWS S3, Google Cloud Storage, Azure Blob) and reference them by URL. Any instance can serve or generate any file this way.
  • Application-level caches. In-process caches (local HashMap, LRU cache) create divergent state across instances. Promote them to a shared distributed cache (Redis, Memcached) so all instances share a consistent view of cached data.
  • Websocket and real-time state. WebSocket connections are inherently stateful — a connected client is attached to a specific server process. Use a pub/sub layer (Redis Pub/Sub, socket.io adapter, Ably) to broadcast events across all instances so any connected client on any server receives the message.

Load balancing strategies

A load balancer is the front door to a horizontally scaled cluster. It takes incoming connections and hands them to a healthy backend instance according to whichever algorithm you have chosen. That algorithm, together with how you configure health checks and connection draining, has an outsized effect on both performance and reliability once real traffic hits.

The most widely used algorithms in production web applications are:

  • Round robin. Requests are distributed sequentially across instances. Statistically equal distribution in theory; in practice, slow requests can cause imbalance if some instances accumulate long-running jobs. Works well when request processing time is roughly uniform (REST APIs, static-asset servers).
  • Least connections. Each new request goes to the instance with the fewest active connections at that moment. Significantly better than round robin when request duration varies widely — for example, when some requests trigger complex database queries while others are lightweight lookups. Used by default in NGINX Plus and AWS ALB weighted target groups.
  • Weighted routing. Instances are assigned weights based on their capacity. A 16-core instance can receive twice the traffic of an 8-core instance in the same pool. Useful in mixed-instance deployments or during canary releases when you want only a fraction of traffic to reach a new version.
  • IP hash / sticky routing. The same client IP always reaches the same instance. Useful for stateful backends where statelessness has not yet been achieved, or for WebSocket connections in single-instance setups. Avoid as a long-term strategy — it limits autoscaling and creates uneven load distribution.

How fast a load balancer notices a dead instance comes down to three health-check settings: the probe interval (how often it checks), the healthy threshold (how many successes in a row mark an instance as good), and the unhealthy threshold (how many failures in a row pull it from rotation). For production web APIs, sensible defaults are an interval of 10–15 s, a healthy threshold of 2, and an unhealthy threshold of 2–3. Worst case, you spot a failure in 30–45 seconds, which most availability budgets can absorb.

Connection draining (also called deregistration delay) is equally important: when an instance is removed from the load balancer pool (during a deploy or scale-down), the load balancer should wait for in-flight requests to complete before terminating the connection — typically 30–60 seconds. Without draining, your deploys create a brief burst of 502/503 errors as active requests hit a terminating instance.

Caching layers: from in-process to CDN

For a backend engineer, caching returns more performance per unit of effort than almost anything else. One cache hit saves a database query that might otherwise cost 5–50 ms, and across millions of requests that adds up fast. It is also one of the easiest patterns to get wrong: teams tend to either under-cache and leave obvious wins on the table, or over-cache and end up serving stale data behind the wrong TTLs with no way to invalidate it.

Think of caching as a stack of layers, each progressively faster and progressively less durable:

  • CDN edge caching. Fully static assets (images, JS bundles, CSS, fonts) should be cached at the CDN edge and served from the nearest point of presence to the user — adding far-future Cache-Control headers (max-age=31536000, immutable) and content-hashed filenames. For partially static content (blog posts, product pages), Stale-While-Revalidate allows the CDN to serve cached responses while asynchronously refreshing in the background. A well-configured CDN absorbs 60–95% of total HTTP requests before they reach your origin.
  • Reverse proxy / gateway caching. An NGINX or Kong gateway layer can cache API responses for short TTLs (1–60 s) when the endpoint is safe to cache and the data changes infrequently. This layer protects your application servers from traffic spikes without requiring application code changes.
  • Distributed in-memory cache (Redis / Memcached). Shared across all application instances, used for computed aggregates, database query results, third-party API responses, session tokens and rate-limit counters. Redis is the industry default in 2026 — it supports richer data structures than Memcached, includes TTL-based expiry, pub/sub for real-time events and optional persistence. Cache invalidation strategy choices: TTL (simple, risks brief staleness), explicit eviction on write (consistent, adds write-path complexity), or event-driven invalidation via a message queue.
  • Application-layer in-process cache. A small in-process LRU cache (node-lru-cache, Caffeine in JVM) for truly hot, rarely changing data — for example, a lookup table of 200 country codes loaded at startup. Extremely fast (nanoseconds vs microseconds for Redis), but must be kept small to avoid memory pressure and must be invalidated correctly when the source data changes.
Multiple servers in a data centre representing horizontal scaling of a web application
A horizontally scaled web tier with a load balancer distributing traffic across multiple stateless instances. Each instance reads from a shared Redis cache and a replicated database, with no per-instance session state.

The metric to watch is cache hit ratio. On your busiest endpoints, a distributed cache should clear 80%. Anything under 70% usually points to one of a few culprits: TTLs set too short, cache keys carved too finely, or a dataset whose hot spots simply need a different access pattern. Cross-reference your database slow query log against cache-miss logs and the highest-value caching opportunities tend to surface on their own.

Async processing and message queues

HTTP is synchronous by nature: the client sits and waits while the server does its work. For most interactions that is exactly right — someone submits a form and expects an answer inside a second. Plenty of backend work, though, has no business running inside that window: sending email, resizing an upload, generating a PDF, reindexing a search record, or calling a sluggish third-party webhook. Block the HTTP response on any of them and you burn server resources, push up the latency the user actually feels, and open a fragile failure mode where one third-party timeout sinks the whole request.

The pattern is straightforward: your web server receives the request, persists the minimum required state (a job record in the database, or a message in a queue), returns HTTP 202 Accepted immediately, and a separate worker process does the heavy lifting asynchronously. The user's browser polls for completion or receives a push notification (WebSocket, Server-Sent Events) when the job finishes.

The message queue (also called a task queue or job queue) is the mechanism that connects the web tier to the worker tier. The most widely used options in production web applications in 2026:

  • Amazon SQS. Fully managed, zero operational overhead, scales automatically to any throughput. Standard queues offer at-least-once delivery; FIFO queues provide exactly-once processing in order. The AWS-native default for teams already on AWS infrastructure.
  • RabbitMQ. Mature, self-hosted or managed (CloudAMQP). Supports sophisticated routing topologies (exchanges, bindings, dead-letter queues) that SQS lacks. Preferred when you need fine-grained message routing or priority queues. Slightly more operational overhead than SQS.
  • Apache Kafka. Log-based, high-throughput, designed for event streaming at millions of messages per second. Retains messages for configurable periods (not just until consumed), enabling replay and fan-out to multiple consumers. The correct choice for event sourcing, analytics pipelines and real-time data processing. Overkill for simple task queues — Kafka's operational complexity is significant.
  • Redis Streams / Bull queue. For teams already running Redis, Bull (Node.js) or RQ (Python) provide simple job queues backed by Redis. Good for moderate throughput (thousands of jobs per minute) with minimal infrastructure addition. Not suitable for high-throughput or multi-consumer fan-out patterns at scale.

Worker scaling and web scaling move independently. In steady state you might run 10 web instances against 3 workers, then push workers to 20 for a batch-processing surge while the web tier stays exactly where it is. That decoupling is one of the real payoffs of an async design: each layer gets sized for the work it actually does.

Database scaling: read replicas and sharding

In a growing web application, the database is nearly always the first thing to buckle. Stateless app servers scale out with little friction; a database carries state, which makes it a much harder thing to grow. The saving grace is that most production apps read far more than they write — 80:20, often 90:10 — so read scaling is usually the first problem worth solving.

Read replicas are the standard opening move. Most managed services — AWS RDS, Google Cloud SQL, PlanetScale — give you one or more replicas: synchronised copies of the primary that take read-only queries. The application sends every SELECT to a replica and every write (INSERT, UPDATE, DELETE) to the primary. A single well-provisioned replica can soak up 70–90% of query load and drop primary CPU in step. Put three or four behind a connection pooler such as PgBouncer or ProxySQL and you can carry the read traffic of very large systems without touching the schema.

The trade-off is replication lag. Async replication (the default in most managed services) means replicas may lag the primary by milliseconds to seconds under write-heavy load. For most read use cases this is acceptable; for reads that must reflect an immediately preceding write (for example, confirming a purchase to a user), route that specific query to the primary using read-your-writes consistency guarantees or with explicit routing logic in your application.

Network diagram illustrating distributed web app architecture with load balancers and multiple database nodes
A distributed web application architecture with load-balanced stateless web servers, a shared cache tier, a primary database with read replicas, and an async worker tier draining a message queue.

Database sharding is the next step when write throughput or dataset size exceeds what a single primary can handle. Sharding splits your data horizontally across multiple database nodes (shards) based on a shard key. Every record is assigned to exactly one shard based on a deterministic function of its shard key — for example, user_id % N where N is the number of shards. Each shard is an independent database with its own primary and (optionally) its own replicas.

Sharding buys scale at a steep price in operational and query complexity. A query that spans shards — a join or aggregation over users who live on different nodes — needs either scatter-gather fan-out or redundant de-normalised data. Every schema change has to land on all shards in lockstep. Rebalancing as data grows is a careful migration in its own right. So sharding is genuinely a last resort: most teams exhaust application-level partitioning, time-series table partitioning (one big table split into monthly or yearly sub-tables), and a managed distributed database such as CockroachDB, Vitess or PlanetScale before they hand-roll a shard layer.

TechniqueSolvesComplexityWhen to use
Query optimisationSlow individual queriesLowAlways first
Connection poolingConnection exhaustionLowWhen connections > 100
Read replicasRead throughputLow-mediumRead:write ratio > 5:1
Table partitioningTable size / query performanceMediumTables > 50M rows
ShardingWrite throughput / total data sizeHighWhen replicas are insufficient
Managed distributed DBAll of the aboveMedium (operational outsourced)Greenfield or migration budget available

Autoscaling and capacity planning

Autoscaling lets your infrastructure adjust its own compute capacity to match demand as it happens, spinning up instances when traffic spikes and shutting them down once it eases. Across cloud platforms — AWS Auto Scaling Groups, Google Cloud Managed Instance Groups, the Kubernetes Horizontal Pod Autoscaler — it is the usual way to hold availability without paying to keep peak-load capacity idle at the baseline.

For autoscaling to behave, three pieces have to line up: signals that actually mean something, thresholds tuned to your workload, and instances that can start and stop cleanly inside the autoscaler's reaction window.

Scaling signals. Most autoscalers reach for CPU utilisation by default: add instances once average CPU crosses a high mark (usually 60–70%), remove them when it falls under a low one (usually 30–40%). For compute-bound work that is a fair proxy, but it misleads on I/O-bound apps that spend their day waiting on the database. Web APIs often scale more accurately on request latency (p95 API response time) or active connection count. When the stock CPU metric keeps lying to you, wire up a custom CloudWatch, Stackdriver or Datadog metric and scale on that instead.

Cooldown and stabilisation windows. After each scaling action an autoscaler waits out a cooldown, which stops it thrashing — flapping instances up and down as the metric wobbles around the threshold. Good starting points are a 60–90 s cooldown on scale-out and a longer 300–600 s on scale-in, since pulling capacity is riskier than adding it. Tune both to your startup time and how spiky your traffic really is.

Capacity planning. Autoscaling does not retire capacity planning so much as reshape it. You stop pre-provisioning for sustained peak, but you still have to set minimum and maximum instance counts that respect both your availability needs and your budget. Keep the minimum at 2 (one per availability zone) so a scale-in never leaves you without redundancy, and cap the maximum at your estimated peak plus roughly 30%, which stops a traffic spike — or a buggy request loop — from scaling you into the ground.

Pre-warming earns its keep when startup time is large next to how long your spike lasts. Containerised apps (Docker on ECS or Kubernetes) usually boot in 5–30 s; VM-based scale-out is more like 60–180 s. When a spike arrives all at once — a marketing email goes out, a product launches — warming instances beforehand, or scheduling the scale-up, keeps that first wave of requests from slamming an under-resourced cluster while the autoscaler is still catching up.

SLOs and graceful degradation

Handling more load is only half of scalability. The other half is holding performance steady as load climbs and recovering cleanly when something breaks. Service Level Objectives (SLOs) are the formal commitments that pin down what "acceptable" means for your system.

A well-chosen SLO does two jobs at once. On the operations side, it sets the line that trips an incident and wakes whoever is on call. On the architecture side, it fixes the performance envelope the system has to sustain, and it tends to expose which component is the real constraint. Say your SLO is p95 API latency under 500 ms and you keep breaching it during database maintenance windows — that is both a reliability problem and an architectural gap, usually thin read-replica capacity or a missing query cache.

Standard SLO targets for production web applications:

  • Availability: 99.9% (Three Nines) allows 8.7 hours of downtime per year. This is the minimum expectation for any commercial SaaS. 99.95% (4.4 hrs/yr) is achievable with multi-AZ deployment and automated failover. 99.99% requires active-active multi-region architecture.
  • API latency: p50 < 150 ms, p95 < 500 ms, p99 < 1,000 ms for synchronous HTTP APIs. These are reference points for well-optimised web APIs; compute-heavy endpoints (ML inference, complex reports) carry their own latency budgets.
  • Error rate: Less than 0.1% of all requests should return 5xx errors during normal operation. A sustained error rate above 0.1% for more than 5 minutes should trigger an alert.
  • Throughput: Define your expected peak RPS (requests per second) and design your infrastructure to sustain it at the SLO latency targets with 30–50% headroom. The headroom absorbs traffic spikes while the autoscaler reacts.

Graceful degradation is the principle that keeps an overloaded application failing predictably instead of falling over outright. When a dependency turns slow or drops offline, the system steps down to a reduced but still-working state rather than crashing. A few patterns that make this concrete:

  • Circuit breakers. If calls to an external service (a third-party API, a non-critical internal microservice) fail repeatedly, open the circuit — short-circuit subsequent calls with a cached fallback or a "service temporarily unavailable" message rather than hammering a failing dependency. Libraries like Resilience4j (Java), Polly (.NET) or Opossum (Node.js) implement this pattern.
  • Load shedding. When request queues exceed a threshold, actively reject low-priority requests with HTTP 429 (Too Many Requests) rather than accepting them into a queue that can never drain. This protects the core request flow from being starved by low-priority background traffic.
  • Feature flags for non-critical paths. Non-essential features (activity feeds, personalisation, recommendation widgets) can be disabled at the feature-flag level during a high-load incident, allowing the application to continue serving core flows without the overhead of the degraded component.
  • Stale data over no data. When a cache miss occurs and the database is slow or unavailable, serving a slightly stale cached response is often preferable to returning an error. Implement serve-stale-on-error policies in your cache layer for read-heavy endpoints where eventual consistency is acceptable.

Every engagement under our web application development services at YuSMP Group folds in architectural review and scalability planning. We build toward the SLOs from day one, settling autoscaling policies, caching strategy and degradation behaviour before the first line of code exists rather than bolting them on after launch. If your team is still weighing architectural approaches, our companion piece on monolith vs microservices covers how that decomposition choice plays into scaling strategy.

FAQ

What is the difference between horizontal and vertical scaling?

Vertical scaling (scale-up) means adding more CPU, RAM or storage to a single server. It is fast to implement but has a hard ceiling and creates a single point of failure. Horizontal scaling (scale-out) means adding more server instances and distributing load across them. It has no theoretical ceiling, eliminates single points of failure, and is the foundation of cloud-native web app architecture.

Why does statelessness matter for web app scalability?

A stateless application stores no per-request session data in memory on any individual server. Every instance can handle any incoming request without coordination. This means you can add or remove instances instantly without routing requests to specific servers — which is the prerequisite for autoscaling and zero-downtime deployments. Stateful servers require sticky sessions, complicating load balancing and preventing true horizontal scale.

When should I add a caching layer to my web app?

Add caching when read traffic dominates — typically when your database CPU exceeds 60% or when the same data is read significantly more often than it changes. Start with an in-process cache for hot lookup tables, then add a distributed cache (Redis, Memcached) when your app runs across multiple instances. A well-tuned Redis cache can absorb 80–95% of read load, reducing database pressure proportionally.

What is database sharding and when is it necessary?

Sharding splits a database table horizontally across multiple servers (shards) based on a shard key — for example, by user ID range or geographic region. It is necessary when your dataset outgrows a single database server's capacity or when write throughput exceeds what one primary can handle. Most teams reach for read replicas first (for read scaling) and only introduce sharding when they hit write bottlenecks at significant scale, typically 10M+ rows with high write rates.

How do message queues improve web app scalability?

Message queues (RabbitMQ, SQS, Kafka) decouple slow or resource-intensive work from the HTTP request cycle. Instead of making a user wait while your server sends emails, resizes images, or calls a third-party API, you enqueue the task and respond immediately. Worker processes drain the queue independently and can be scaled separately from your web tier — meaning a spike in background work does not slow down interactive requests.

What SLOs should a scalable web app target?

Industry-standard targets for a production SaaS are: 99.9% uptime (8.7 hrs/yr downtime budget), p50 API latency under 150 ms, p95 under 500 ms, and p99 under 1,000 ms. Error rate should stay below 0.1% of all requests. For consumer-facing apps where UX is critical, tighten p95 to under 300 ms and track Core Web Vitals (LCP under 2.5 s, INP under 200 ms) alongside backend SLOs.

Published 13 June 2026. Technical recommendations reflect production patterns observed across YuSMP Group client engagements 2023–2026 and current cloud provider documentation. Specific thresholds and metrics will vary by workload — treat these as informed starting points, not universal rules.