Decorative background gradient
Back to Blog
System DesignLoad BalancingScalability

Load Balancing and Stateless Service Design — A Practical Guide

How load balancers distribute traffic, why stateless services are what actually makes horizontal scaling work, and how to fix the sticky-session traps that quietly reintroduce state.

Load Balancing and Stateless Service Design — A Practical Guide

Load balancing gets talked about as if it were the hard part of scaling a service horizontally. It isn't — distributing requests across servers is a solved problem with well-understood algorithms. The actual hard part, the one that determines whether adding more servers helps at all, is making sure your service doesn't depend on state that only exists on one specific server.

Why Statelessness Comes Before Load Balancing

Imagine a server that stores a user's shopping cart in memory after login.

ts

Add a second server behind a load balancer, and this breaks immediately: a user's cart-add request might land on server A, and their next request — checking out — might land on server B, which has never seen their cart. No load balancing algorithm fixes this; the problem is architectural, not routing. The fix is moving cart state out of process memory and into a shared store every instance can read.

ts

Now any server instance can serve any request for any user, because the state that matters lives outside the process. This is the actual definition of a stateless service — not "has no state," but "holds no state a later request depends on that isn't externally accessible."

Load Balancing Algorithm 1: Round-Robin

Round-robin distributes requests to backend servers in a fixed rotating order.

text
nginx

It's simple and works well when requests take roughly the same amount of time to process. It breaks down when request costs vary widely — a server that happens to get a run of expensive requests keeps receiving new ones at the same rate as an idle one, since round-robin has no concept of current load.

Load Balancing Algorithm 2: Least Connections

Least-connections routes each new request to whichever backend currently has the fewest active connections.

nginx

This adapts to uneven request costs far better than round-robin, since a server bogged down with slow requests naturally receives fewer new ones until it catches up. It's the better default for most real APIs, where request duration varies (a search query versus a health check, for example).

Load Balancing Algorithm 3: Consistent Hashing

Consistent hashing routes requests based on a hash of some request property (often a user or session ID), so the same key consistently lands on the same backend — useful for cache locality without full statefulness.

ts

The distinction from sticky sessions matters: consistent hashing is a routing optimization (so a user's requests tend to hit a server that already has their data warm in a local cache), not a correctness requirement. If that server goes down, the request routes elsewhere and still works correctly — it's just a cache miss, not a broken session. This is the difference between hashing as an optimization and stickiness as a crutch for state that should have been externalized.

Why Sticky Sessions Are a Trap

Sticky sessions configure the load balancer to always route a given user to the same backend server, typically because that server holds the user's session in local memory.

nginx

This looks like it solves the statelessness problem, but it only hides it. Three real consequences follow: if that specific server crashes, every user stuck to it loses their session entirely. Load distribution becomes uneven over time, since some servers accumulate more "sticky" long-lived users than others, regardless of current load. And scaling down — removing a server during a deploy or traffic dip — forcibly evicts every session pinned to it. Fixing the actual problem (externalize the session to Redis or a database) removes the need for stickiness entirely, and is almost always less work long-term than operating around it.

Health Checks: Making the Load Balancer Aware of Failure

None of the above matters if the load balancer keeps sending traffic to a server that's crashed or degraded.

ts
nginx

The load balancer polls this endpoint on an interval and stops routing new requests to any instance that fails enough consecutive checks, resuming once it recovers. A meaningful health check verifies the dependencies that actually matter to serving requests (database connectivity, for instance) — a health check that only confirms the process is running will report "healthy" even when the server can't actually do its job.

Key Takeaways

Load balancing algorithms are the easy part — round-robin for uniform request costs, least-connections for variable ones, and consistent hashing when you want cache locality without correctness depending on it. The precondition that makes any of this work is a genuinely stateless service, where session and application state live in a shared store rather than server memory; sticky sessions are a sign that precondition hasn't been met, not a valid substitute for it. And a load balancer without real health checks will happily keep sending traffic to a dead server, so health checks that verify actual dependencies (not just process liveness) are not optional in production.

Frequently Asked Questions

What does it mean for a service to be stateless?

A stateless service doesn't store any data in server memory that a later request depends on — every request carries or can retrieve everything it needs from an external store (a database, cache, or token). This means any instance of the service can handle any request, which is the actual precondition for horizontal scaling to work.

What's the difference between round-robin and least-connections load balancing?

Round-robin distributes requests to servers in a fixed rotating order, regardless of how busy each server currently is — simple, but can overload a server that's slow to finish requests. Least-connections sends each new request to whichever server currently has the fewest active connections, which adapts better when requests take varying amounts of time to process.

Why are sticky sessions considered an anti-pattern?

Sticky sessions route a specific user to the same backend server on every request, usually to work around that server holding the user's session in memory. This defeats load balancing's purpose — if that server goes down, the user's session is lost, and traffic distribution becomes uneven since some servers accumulate "stickier" users than others.

How do load balancers detect a failed server?

Through health checks — the load balancer periodically sends a request (often to a dedicated /health endpoint) to each backend server and stops routing traffic to any server that fails to respond correctly within a configured threshold. Without health checks, a load balancer keeps sending real user traffic to a crashed instance until client-side timeouts mask the problem.

Working on something similar? Take a look at my services and case studies, or book a free call to talk about your idea.

Related Articles

Trending Topics