Back to Blog
Lesson 22 of the System Design: System Design Fundamentals course
ArchitectureAugust 8, 20264 min read

Horizontal Scaling and Load Distribution: A Practical Guide

Learn how to scale your system horizontally. We cover stateless architecture, auto-scaling groups, and session affinity to handle massive traffic growth.

system designscalabilityinfrastructureload balancingstateless
High angle shot of neatly stacked wooden pallets in an outdoor warehouse setting.

Previously in this course, we explored Vertical Scaling Strategies: Optimizing Server Resources and the fundamentals of traffic management in The Role of the Load Balancer: Scalability and Traffic Management. While upgrading hardware has a ceiling, horizontal scaling—adding more servers to your fleet—allows your system to grow indefinitely.

In this lesson, we move beyond single-node constraints to build systems that can expand or contract based on real-time demand.

Understanding Stateless Service Architecture

The bedrock of horizontal scaling is statelessness. A service is stateless if it does not store client-specific data (like user sessions or temporary file uploads) in its local memory or disk.

When a service is stateless, any request can be handled by any server in your pool. If you have five servers and one goes down, the load balancer simply redirects traffic to the remaining four. If you need more capacity, you spin up a sixth, and it immediately begins processing requests without needing prior knowledge of the user's history.

The Problem with State

If your application stores state locally (e.g., in a Map object in memory), a user might be routed to Server A to log in, but then routed to Server B for their next request. Because Server B doesn't have the login state, the user is forced to log in again. This is why scaling fails if your architecture is not strictly decoupled from local state.

Configuring Auto-Scaling Groups (ASG)

A close-up view of PHP code displayed on a computer screen, highlighting programming and development concepts.

Horizontal scaling is rarely done manually. Instead, we use Auto-Scaling Groups. An ASG is a cloud construct that monitors your fleet’s health and resource utilization. When CPU or memory usage crosses a threshold (e.g., 70%), the ASG automatically provisions new instances from a pre-configured template.

The Lifecycle of an ASG

  1. Launch Template: Defines the image, instance type, and networking for your nodes.
  2. Metrics: The ASG watches for triggers (e.g., high average CPU).
  3. Scaling Action: The group spins up new nodes or terminates underutilized ones.
  4. Registration: The new nodes register with your Nginx load balancer to begin receiving traffic.

Managing State with Session Affinity

Sometimes, you cannot fully eliminate state immediately. In these cases, we use Session Affinity (also called "sticky sessions"). This tells the load balancer to route a specific user’s requests to the same physical server for the duration of their session.

While this solves the immediate problem, it creates "hot spots"—where one server handles more load than others—and complicates deployments. Whenever possible, move state out of the application layer entirely using a shared external store, as described in Session Persistence in Clusters: Scaling Laravel Infrastructure.

Comparison: Handling State

StrategyImplementationScalability
Local StateStore in memoryPoor (Breaks scaling)
Session AffinitySticky load balancer cookiesMedium (Risk of hot spots)
External StateRedis/Database clusterHigh (True stateless)

Worked Example: Stateless Design

Imagine a Node.js service that keeps a user's "last viewed item" in memory. To make this horizontal-ready, we refactor it to use an external store.

Before (Stateful):

JAVASCRIPT
const sessionStore = {}; // Memory leak and scaling blocker

app.post(CE9178">'/view', (req, res) => {
  sessionStore[req.user.id] = req.body.itemId;
  res.send(CE9178">'Updated');
});

After (Stateless):

JAVASCRIPT
// Using Redis as an external source of truth
const redis = require(CE9178">'redis');
const client = redis.createClient();

app.post(CE9178">'/view', async (req, res) => {
  await client.set(CE9178">`user:${req.user.id}:last_viewed`, req.body.itemId);
  res.send(CE9178">'Updated');
});

Hands-on Exercise

  1. Identify State: Review your project’s current design document. List every component that currently relies on local memory or filesystem storage.
  2. Refactor Plan: Document a plan to move one of those components to a shared data store (like Redis or your main database).
  3. Draft ASG Policy: Write a short paragraph defining the scaling criteria for your service (e.g., "Scale out when CPU > 75% for 5 minutes, scale in when < 30% for 10 minutes").

Common Pitfalls

  • Ignoring "Scale-In" costs: If your service takes 5 minutes to boot, your scale-in threshold must be conservative to avoid killing nodes while the system is still busy.
  • Shared Disk Dependency: Relying on the local filesystem for file uploads will fail in a multi-node setup. Always use Object Storage (like S3).
  • Over-stickiness: If you use session affinity, ensure you have a fallback mechanism for when a node dies, otherwise, you force an error on the user.

FAQ

  • Is horizontal scaling always better? No, it adds complexity. Only scale horizontally once your vertical limits are reached or you need high availability.
  • Does stateless mean I can't use databases? No. Statelessness refers to the application server. Your database remains the source of truth for persistent data.

Recap

Horizontal scaling is the practice of distributing load across multiple nodes. By ensuring your services are stateless and utilizing Auto-Scaling Groups, you can build systems that adapt to traffic patterns automatically.

Up next: We will dive into Database Replication to ensure our data tier can keep up with our newly scaled application tier.

Similar Posts