26.7. Load Balancing
Table of Contents
Why Load Balancing Matters
When your backend grows, a single server often becomes a bottleneck. At some point:
- The CPU is busy most of the time.
- Requests become slower during peak usage.
- A crash brings down the whole application.
Load balancing solves this by spreading incoming requests across multiple backend instances. Instead of one server doing all the work, several servers share it, and a load balancer sits in front to decide which one handles each request.
You can think of the load balancer as a receptionist in a busy clinic: patients come in through one door, but the receptionist sends each patient to one of several doctors so no single doctor is overwhelmed.
Load balancing is central for:
- Scalability: add more instances to handle more traffic.
- High availability: if one instance fails, others can keep serving requests.
- Performance: reduce latency by avoiding overloaded instances.
In this chapter, we focus on these aspects and on the strategies used to distribute load, not on how to configure specific tools like Nginx or cloud load balancers, which are covered elsewhere.
Basic Load Balancing Architecture
A typical backend with load balancing looks like this:
+----------------------+
Clients ---> | Load Balancer | ---> [ App Server 1 ]
(browsers, +----------------------+ [ App Server 2 ]
mobile apps) | | [ App Server 3 ]
v v
multiple backend
instancesKey roles:
- Client: sends HTTP requests, usually to a single publicly known domain.
- Load balancer: receives all requests to that domain, then forwards them to one of many backend instances.
- Backend instances: run your application code, typically stateless REST APIs.
Important details:
- Clients usually do not know how many backend instances exist.
- The load balancer can be a separate process (e.g., Nginx, HAProxy, AWS ALB, cloud L7 balancer) or part of a reverse proxy layer.
- If the app is stateless, it is easy to add or remove instances behind the load balancer.
Key Requirement
For simple, reliable load balancing, application servers should be stateless. Store user state in shared systems (database, Redis, etc.), not in local memory that depends on a specific server.
Horizontal Scaling With Load Balancing
Vertical scaling means "bigger server." Horizontal scaling means "more servers." Load balancing is the enabler for horizontal scaling.
Vertical vs horizontal scaling
| Approach | How it scales | Pros | Cons |
|---|---|---|---|
| Vertical scaling | Add more CPU, RAM to one machine | Simple to manage, no code changes | Hardware limits, single point of failure |
| Horizontal scaling | Add more instances, same app | High scalability, high availability | More complex infrastructure and design |
Load balancing is necessary for horizontal scaling because:
- Clients should not need to know about every server address.
- You need a central place to decide which server should handle a request.
- You want to detect unhealthy instances and avoid sending traffic to them.
Scaling out and back in
With a load balancer, you can:
- Scale out: Add more instances when traffic grows.
- Scale in: Remove instances when traffic drops to save cost.
In many cloud environments, autoscaling works like this:
- A metric crosses a threshold (for example, CPU > 70% for several minutes).
- Autoscaling adds a new instance.
- The load balancer detects the new healthy instance and starts sending traffic to it.
Later, when load decreases:
- CPU stays low for some time.
- Autoscaling removes instances one by one.
- The load balancer stops sending traffic to instances being terminated.
You need to design your app so it tolerates instances appearing and disappearing at any time.
Common Load Balancing Algorithms
The core job of the load balancer is to decide which backend instance should handle a new request. This decision is made by a load balancing algorithm.
Below are the most common ones. In practice, you often combine an algorithm with health checks and weights (for example, some servers are stronger, so they get more traffic).
1. Round Robin
Round robin cycles through servers in order.
Example with 3 servers: A, B, C
Requests are distributed as:
- Request 1 β A
- Request 2 β B
- Request 3 β C
- Request 4 β A
- Request 5 β B
- Request 6 β C
... and so on.
Pros:
- Very simple.
- Works well when all servers are similar and requests have similar cost.
Cons:
- Does not consider actual load (CPU, memory, queue length).
- A slow or overloaded server gets the same number of new requests.
Weighted round robin
You can assign weights based on server capacity.
Suppose:
- Server A weight = 2
- Server B weight = 1
Then sequence might be:
- A, A, B, A, A, B, ...
Server A gets about $\frac{2}{2+1} = \frac{2}{3}$ of the traffic, server B gets $\frac{1}{3}$.
Important Rule
Use weighted round robin when your servers have different capacities. Otherwise slower or smaller servers can be easily overloaded.
2. Least Connections
The next request goes to the server with the fewest active connections.
Example:
- Server A: 50 active connections
- Server B: 30 active connections
- Server C: 10 active connections
New request goes to Server C.
Pros:
- Adapts to differences in request duration. Long-running requests keep connections open, so busy servers receive fewer new connections.
- Better for cases where request times vary a lot.
Cons:
- Requires tracking active connections.
- For very short-lived requests, the benefit over round robin may be small.
Least response time (variation)
Some load balancers use a variation: least response time, which considers:
- Number of active connections, and
- Average response time per server.
This sends more requests to servers that answer quickly.
3. IP Hash (or consistent hashing)
The server is chosen by applying a hash function to the client IP address.
Example:
server_index = hash(client_ip) mod NWhere:
hash(client_ip)is a deterministic hash function.Nis the number of backend servers.
Effect:
- The same client IP tends to reach the same backend server across requests.
- If you have 3 servers, different IPs map to different servers based on the hash function.
Pros:
- Useful for basic session stickiness when you cannot use another mechanism.
- Distribution is often reasonably balanced.
Cons:
- If the number of servers changes, many clients remap to different servers.
- Not ideal behind NAT where many users share one IP.
Consistent hashing
A more advanced form is consistent hashing, which minimizes how many clients move when servers are added or removed. It is often used in distributed caches (for example, Redis clusters) but can also be used in load balancing.
4. Random
Each request is sent to a random server.
You can make this weighted as well:
- 60% chance to go to A
- 40% to go to B
Pros:
- Simple.
- With enough traffic, random distribution is close to even.
Cons:
- No awareness of load.
- Short term bursts can hit the same server by chance.
5. Custom / application level strategies
Sometimes the application itself participates in load distribution, for example:
- Use a service discovery system to know available instances.
- Apply custom routing logic based on:
- Tenant ID
- Region
- Feature flags
- Data partitioning (for example, specific shard)
In such cases, a simple algorithm (round robin) at the network level is combined with logic in the app that uses other systems (like a service registry, or database sharding rules).
Application-Level Concerns: Statelessness and Sessions
The balancing strategy interacts strongly with how you manage state.
Stateless vs stateful servers
- Stateless server: Each request contains everything the server needs. State lives in shared systems (database, cache, external storage).
- Stateful server: Important data is kept in memory on a particular server, for example:
- In-memory sessions.
- In-memory shopping carts.
- In-memory feature flags or temporary tokens stored only on that instance.
Load balancing is far easier with stateless servers, because:
- Any server can handle any request.
- You can add or remove instances without breaking user sessions.
- It simplifies autoscaling and failure recovery.
Session stickiness (affinity)
Sometimes, you still have stateful behavior, like sessions. In that case, you may need session affinity or sticky sessions.
Sticky sessions mean: the same user is consistently routed to the same backend instance.
Common approaches:
| Method | How it works | Pros | Cons |
|---|---|---|---|
| IP hash | Map client IP to a server using a hash | Simple, no extra data | Breaks with NAT, clients changing IP |
| Cookie-based | Load balancer sets a cookie that encodes the target | More reliable than IP-based | If target server dies, session may break |
| App-session based | App issues its own session ID that includes server ID | Fully controlled by application | More complex, can become brittle |
In practice, stateless sessions are preferred for scalable APIs:
- Use JWT or a session ID stored in a shared data store like Redis.
- Any server can verify or retrieve the session.
Best Practice
Design your backend so that any request can be processed by any instance. Avoid server-local sessions. Use shared storage (database, Redis, etc.) and stateless tokens.
Health Checks and Failover
A critical part of a real load balancing setup is health checking.
What is a health check?
A health check is a periodic request to each backend instance to verify that it is healthy.
Typical patterns:
- Load balancer sends HTTP GET
/healthevery few seconds. - The app responds with
200 OKif healthy, or5xx/ timeout if unhealthy.
If a server fails health checks:
- Load balancer stops sending traffic to it.
- When health checks pass again, it can be added back.
Simple vs deep health checks
- Simple check: Just verify that the application process is running.
- Example: Returns
200 OKif the web server responds. - Deep check: Also verify dependencies:
- Can it connect to the database?
- Is Redis reachable?
- Are required services available?
You can also have multiple levels:
/health/live(liveness): Is the app process alive?/health/ready(readiness): Is the app ready to receive traffic?
Load balancers often use a readiness endpoint to avoid sending traffic to instances that are starting up, migrating, or overloaded.
Failover
When an instance fails:
- Health checks fail repeatedly.
- Load balancer marks it as unhealthy.
- Traffic is automatically redistributed to other instances.
If all instances are down, the load balancer might:
- Return
502 Bad Gatewayor503 Service Unavailable. - Optionally serve a static maintenance page.
Your job as a backend engineer is to:
- Expose clear health endpoints.
- Make sure they reflect true readiness.
- Make them fast and lightweight, but not misleading.
Layer 4 vs Layer 7 Load Balancing (Conceptual)
Without diving deep into networking chapters, it helps to understand that load balancers can operate at different layers.
- Layer 4 (transport): Balancer operates on TCP/UDP connections.
- Example: Balance TCP port 80 traffic without understanding HTTP.
- Fast and simple, limited routing flexibility.
- Layer 7 (application): Balancer understands HTTP (or other application protocols).
- Can route based on URL path, headers, cookies, etc.
- Example: Requests to
/apigo to one backend,/staticto another.
Layer 7 features you will commonly use:
- Path-based routing:
/api/v1/β API service,/assets/β static server. - Header-based routing: A/B testing, canary releases.
- Cookie-based session stickiness.
These options influence how you design your backend routes and endpoints.
Load Balancing and Performance Considerations
Load balancing itself has performance implications.
Overhead and single point of failure
You are adding an extra hop:
Client β Load Balancer β BackendThis introduces:
- Some additional latency per request.
- New potential single point of failure.
To mitigate:
- Use an efficient load balancer.
- In production, often use multiple load balancer instances with their own redundancy, or managed load balancer services.
- Keep your load balancer configuration simple and well tested.
Connection pooling and keep-alive
Modern load balancers:
- Keep long-lived TCP connections to backends open.
- Reuse them for many HTTP requests (keep-alive).
Benefits:
- Reduced overhead for TCP/TLS handshakes.
- Better throughput.
As a backend engineer, you should:
- Make sure your application supports HTTP keep-alive.
- Configure appropriate max connections and timeouts.
- If using app servers like Gunicorn, Uvicorn workers, or similar, tune worker counts and concurrency for expected traffic.
Balancing CPU-bound and I/O-bound workloads
Load balancing does not eliminate internal performance issues:
- CPU-bound endpoints can still overload individual instances.
- I/O-bound endpoints can be limited by database or external service capacity.
You often combine:
- Load balancing across multiple app instances.
- Database optimization and connection pooling.
- Caching frequently requested data.
Load Balancing and Microservices
In a microservices architecture, load balancing happens more than once.
Examples:
- Edge / API gateway layer:
- Clients access a single public endpoint.
- The gateway or reverse proxy balances across copies of a gateway service.
- Internal service-to-service:
- One service calls another.
- It can use:
- A central load balancer, or
- Client-side load balancing, where the client library knows multiple instances and chooses one to call.
- Database / cache clusters:
- Requests are distributed across database replicas or cache nodes.
- Often use separate, database-specific balancing methods.
As a backend engineer, you must:
- Know which layer is responsible for load balancing which traffic.
- Ensure each service is horizontally scalable when needed.
- Make services stateless so they work well behind a load balancer.
Practical Design Guidelines
Here are concrete rules you can apply when designing backends that will sit behind a load balancer.
1. Design for statelessness
- Store user sessions and other important state in:
- Databases
- Redis
- Other external systems
- Do not keep critical state only in memory of a single instance.
- Ensure any instance can handle any request.
2. Provide robust health endpoints
- Implement at least:
- A liveness probe: "Is the app process alive?"
- A readiness probe: "Can the app serve full traffic right now?"
- Make sure health endpoints:
- Are fast, simple.
- Reflect real issues (for example, fail readiness if DB is unreachable).
3. Choose algorithms appropriately
- When servers are identical and load is uniform: round robin is usually fine.
- When request durations vary or some servers are busier: least connections can give more stable latency.
- When you must keep some stickiness: IP hash or cookie-based methods.
4. Make scaling safe
- Ensure no long-running in-memory tasks that might be killed if instance is removed. Use background workers and queues for heavy tasks.
- Make deployments and restarts graceful, giving in-flight requests time to finish before shutting down (covered more in deployment and production chapters).
5. Observe and measure
- Collect metrics: per-instance latency, error rates, CPU, memory.
- Identify overloaded instances early.
- Use logs and tracing to see if some instances behave differently.
Simple Example Scenarios
To make the concepts concrete, here are small example scenarios you might encounter.
Scenario 1: Sudden traffic spike
- Your API is behind a load balancer and has 3 instances.
- A marketing campaign brings 5x traffic for 10 minutes.
- CPU on each instance rises from 30% to 90%.
What happens with a proper setup:
- Autoscaling triggers, adds 3 more instances.
- Load balancer sees new healthy instances and includes them in routing.
- CPU per instance stabilizes at a lower value.
- After traffic drops, some instances are removed.
Without load balancing and multiple instances:
- That same spike might overload your single server.
- Users experience slow responses or timeouts.
Scenario 2: One bad instance
You have 4 instances:
- A, B, C, D.
- Instance C has a bug and crashes or runs out of memory.
With health checks and load balancing:
- Health checks to C start failing.
- Load balancer marks C as unhealthy.
- New requests are sent only to A, B, and D.
- Users notice little or no disruption.
Without a load balancer:
- If you had only one instance, your whole service would be down.
Scenario 3: Sticky sessions with legacy design
Legacy app stores sessions in local memory, not in a shared store.
- You have 2 backend servers, X and Y.
- Load balancer uses round robin.
User logs in:
- First request goes to X, session is created in X's memory.
- Next request goes to Y, which has no record of the session, user appears logged out.
Solution:
- Use a load balancing mode with sticky sessions (for example, cookie-based).
- All requests for the same session ID go to X.
- Long term, refactor sessions to a shared store like Redis so stickiness is no longer required.
By understanding how load balancing works conceptually and how it interacts with statelessness, sessions, health checks, and scaling, you can design backend services that perform well and remain available, even under high traffic or partial failures.
Views: 7
KAHIBARO