Load Balancing and Types of Load Balancers Explained
-
Last Updated: July 1, 2026
-
By: javahandson
-
Series
A clear guide to the types of load balancers in system design: server-side, DNS-based, and client-side. Learn how they chain together, L4 versus L7, the common algorithms, and sticky sessions.
One server takes you only so far. Traffic grows, and most teams answer by running several copies of the same service. We call that horizontal scaling, and it fixes the capacity problem well.
But it raises a fresh question right away. With five servers instead of one, something must decide which server takes each request.
That something is a load balancer. Think of a busy restaurant with several chefs. All that cooking power still needs a host at the door who seats each guest at a free table.
Load balancing spreads incoming traffic across many servers so no single machine drowns. It sounds simple on the surface. Yet the details shape almost every large system you will ever design.
You do not need Amazon-sized traffic to hit this. A modest app can outgrow one instance the day it goes viral. The moment a second server appears, load balancing stops being optional.
We start with why this matters, then name every place a routing decision can happen. Here is the plan:
Three separate wins come out of this one component. Each one alone would justify the effort.
Picture a website running on three servers behind one public address. Users neither know nor care that three exist. They type a URL and expect a page.
Somebody has to catch that request and hand it to one of the three. Without that layer, you would hand every user a different address per server. That defeats the whole point of scaling out.
Load balancing hides all of it. Users see a single address, while the work quietly spreads across many machines. Horizontal scaling gives you the servers, and load balancing makes them behave like one service.
There is a resilience angle too. When a load balancer sees that a server is sick, it simply stops sending traffic there.
Users never spot the failure. Their requests land on a healthy server instead, and the page loads as usual.
This is why interviewers love the topic. It touches scalability, availability, and performance all at once.
Money enters the picture as well, though people often skip it. Without load balancing, teams buy one giant server to leave headroom for spikes.
That machine then sits mostly idle outside peak hours. You pay for the peak every hour of every day.
Several right-sized servers work out cheaper. You scale the fleet up and down with demand, and you stop paying for silence at 3 a.m.
A load balancer is anything that decides which backend server handles a request. The client does not get to choose.
That decision can happen in three genuinely different places. Two separate questions hide here, so keep them apart:
Most people picture this model by default. A dedicated component sits between clients and a group of backend servers.
It catches every incoming request, picks a backend, and forwards the request there. The response usually travels back the same way, so all traffic flows through this one component.
Our restaurant host fits here exactly. The host greets each guest, checks which tables are free, and seats people so no chef gets buried.
Server-side balancers come in three flavours. All three sit in the request path and forward traffic, so treat them as one model built three ways:
Whichever you pick, the job stays identical. Sit between clients and servers, choose a healthy backend, forward the request.
Choosing that backend still needs a rule. Send requests in rotation, or pick the least busy server. Those rules are the algorithms in section 6.
DNS-based balancing works much earlier in the journey. It acts before any connection opens, and before a server-side balancer ever sees the request.
When a client looks up a domain name, the DNS server can hand different clients different IP addresses for the same domain. Round-robin DNS rotates through a list. GeoDNS returns the data centre nearest the caller.
This gives DNS a very different character. Nothing proxies the traffic. DNS hands out an address and then steps out of the picture.
Two limits matter. DNS routes to a whole region, never to one server. And it reacts slowly to failure, because browsers, operating systems, and resolvers all cache the answer for the record’s time-to-live.
Lowering that time-to-live speeds up failover. It also makes every client re-query far more often, which piles load onto your DNS infrastructure.
Despite the limits, DNS often forms the very first layer in a global system. It picks the region, then a balancer inside that region takes over.
Client-side balancing flips the model. No shared box sits in the middle at all.
Instead, the caller keeps its own list of available instances and picks one directly. A service registry such as Eureka or Consul usually supplies that list.
Removing the middle box removes a network hop. The caller talks straight to an instance, which cuts latency and drops one component that could fail.
You pay for that in complexity. Now the caller must discover instances and choose among them, so the client gets a little smarter and a little heavier.
A warning about the name, because it catches almost everyone. In web development, “client” usually means the browser or the user’s device.
So client-side load balancing sounds like the browser choosing a server. That is not what it means, and browsers essentially never do this.
The word is relative. It simply means whoever makes a given request.
Most of the time that caller is another backend service. If an Order Service calls an Inventory Service, then Order is the client for that call, even though Order is itself a server in your data centre.
The caller need not live in the backend at all. Two more examples show the same idea outside the data centre:
What ties these together is trust. Every one of these callers is code you wrote and shipped, whether a backend service, your own app, or your own SDK.
A browser is the one caller that never does this, and good reasons explain why:
So the browser always talks to one stable public endpoint. A server-side balancer handles it from there, and client-side balancing stays reserved for callers you trust.
Spring Cloud LoadBalancer is the standard tool here. One microservice calls another by a logical name, and the library resolves that name to a healthy instance.
// An Order Service calling an Inventory Service by name.
// Here the Order Service is the "client", not any browser.
@Service
public class InventoryClient {
private final WebClient webClient;
public InventoryClient(WebClient.Builder builder) {
// "inventory-service" resolves via the service registry,
// then Spring Cloud LoadBalancer picks a healthy instance.
this.webClient = builder.baseUrl("http://inventory-service").build();
}
public Mono<Integer> getStockLevel(String sku) {
return webClient.get()
.uri("/stock/{sku}", sku)
.retrieve()
.bodyToMono(Integer.class);
}
}One caveat before you assume this is the only way. Internal traffic has other options:
No single approach wins outright. The trade-off runs between simplicity and control, and most modern systems mix an edge balancer with one of these internal mechanisms.
These three are not rivals. You do not pick one and drop the rest.
In a big system they work as a team, each handling a different step of the journey. Once you see them chain up, the topic stops feeling like a pile of tricks.
Two walkthroughs make the chain concrete. The first follows a browser, where all three appear in their usual forms. The second follows a mobile app, so you do not leave thinking client-side always means service-to-service.
A user in London opens a shopping site. The company runs it in two regions, one in Europe and one in North America. Here is the path, step by step:
Each step makes a narrower choice than the one before. DNS picks a region, the edge picks a public-facing server, the internal step picks one instance.
No single step could cover the others. DNS cannot read a URL path. The internal balancer has no clue which region the user sits in. They only work as a chain.
Now change one thing. The user opens the company’s own native app instead of a browser.
That difference matters. A mobile app is trusted code the company wrote and shipped, so it can take on a job no browser could.
Say the backend exposes two or three public gateway addresses, one per region. The company bakes that short list into the app:
Step 1 carries the lesson. A caller holding a list and choosing from it is client-side balancing, whether that caller runs on a phone or in a rack.
So client-side does not mean inside the backend. It means the caller chooses, wherever the caller happens to run.
Seeing the chain makes failure easy to reason about. Each layer fails its own way and recovers on its own clock:
A pattern emerges. Lower, finer layers recover fast but cover small failures, while higher, coarser layers recover slowly and cover big ones.
Good designs lean on each layer for what it handles best. Expecting any single layer to catch everything is how outages happen.
| Type | Where in the journey | Granularity | Recovers from |
|---|---|---|---|
| DNS-based | Before the connection opens, during name lookup | Region or data centre | A whole region failing (slowly) |
| Server-side (edge) | At the region entry point, in the request path | One user-facing server | A single server failing (fast) |
| Client-side | Inside the caller: a service, phone, or SDK | One instance of a called service | A single instance failing (fast) |
Sketching this chain early in an interview pays off. It signals that you see load balancing as stages rather than one magic box.
Now for the distinction that comes up in almost every interview.
First, connect this back to the three types. L4 versus L7 is not a fourth type sitting beside them.
It is a property of server-side balancers only. The label tells you which network layer the in-path balancer inspects.
DNS balancing happens before a connection exists, so it has no L4 or L7 to speak of. Client-side balancing keeps the decision inside the caller rather than a separate box.
A Layer 4 balancer works at the transport layer. It sees IP addresses and TCP or UDP ports, nothing more.
Whether the connection carries HTTP, a database protocol, or something exotic makes no difference to it. It forwards packets to a chosen backend and holds the connection open.
Doing so little keeps it blazing fast and cheap on CPU. Reach for L4 when you want raw speed and the routing choice ignores content. The AWS Network Load Balancer and IPVS are common examples.
A Layer 7 balancer works at the application layer. For web traffic it reads the whole HTTP request: the path, the headers, the cookies, even the body.
That opens the door to content-aware routing. Send every /api/orders request to the order service, and every /api/users request to the user service.
It can do more besides. Terminating SSL, rewriting headers, and shifting a slice of traffic to a new version for an A/B test all live here. NGINX, HAProxy in L7 mode, the AWS Application Load Balancer, and Spring Cloud Gateway all fit.
Intelligence costs CPU. Parsing HTTP takes real work compared to blindly forwarding packets, so an L7 balancer handles fewer requests per core.
Most microservice architectures still choose L7 anyway. Path-based routing and SSL termination usually earn back that small cost.
| Aspect | Layer 4 (transport) | Layer 7 (application) |
|---|---|---|
| Sees | IP address and port only | Full HTTP request: path, headers, cookies |
| Routes on | Connection details | Request content |
| Speed | Very fast, low CPU | Slower, higher CPU |
| SSL termination | Not aware of it | Can terminate SSL itself |
| Path-based routing | Not possible | Possible, such as /orders vs /users |
| Examples | AWS NLB, IPVS | NGINX, HAProxy, AWS ALB, Spring Cloud Gateway |
A rule of thumb for interviews: any mention of routing by URL path, hostname, or header means Layer 7. Spreading raw TCP connections as fast as possible means Layer 4 is plenty.
Large systems often refuse to choose. A common pattern puts an L4 balancer at the very edge, since it swallows huge volumes of raw traffic cheaply.
Behind it sit L7 balancers doing the smart, content-aware routing to individual services.
Cloud product names mirror this split. You will often find an AWS Network Load Balancer in front of a fleet of Application Load Balancers, or in front of a Kubernetes ingress controller that handles the L7 work.
Recognising the two-tier pattern shows depth. It proves you see L4 and L7 as partners rather than competitors.
A balancer still needs a rule for choosing among healthy backends. Here is the tour.
Every algorithm above chooses only from servers currently marked healthy. The algorithm and the health check work as a pair.
Health checks decide who is eligible. The algorithm decides which eligible server gets the next request.
So a perfectly tuned algorithm cannot save you. If health checks miss a dead server, the algorithm keeps feeding it traffic.
| Algorithm | Best for |
|---|---|
| Round robin | Servers with equal capacity |
| Weighted round robin | Mixed or uneven hardware |
| Least connections | Long-lived or variable-duration requests |
| Least response time | Latency-sensitive services |
| IP hash | Simple session affinity |
| Consistent hashing | Caches and sharded systems, minimal reshuffling |
| Random | Many balancer instances with no shared state |
A tiny round-robin selector shows the core idea. Keep a list and a pointer, then advance the pointer per request.
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class RoundRobinBalancer {
private final List<String> servers;
private final AtomicInteger index = new AtomicInteger(0);
public RoundRobinBalancer(List<String> servers) {
this.servers = List.copyOf(servers);
}
public String nextServer() {
// floorMod keeps this correct after the counter overflows
// past Integer.MAX_VALUE and turns negative.
int i = Math.floorMod(index.getAndIncrement(), servers.size());
return servers.get(i);
}
}Note the floorMod call. A plain % goes negative once the counter wraps past its maximum, and then get() throws.
A production balancer wraps this same loop in health checks, weights, and careful thread safety.
Load balancing assumes any server can serve any request. That assumption cracks the moment a server keeps something in memory about one user.
A user logs in, and the server stores their session in local memory. Their next click lands on a different server.
That second server never saw the login. So it treats the user as a stranger and bounces them to the login page.
Shopping carts break the same way. Mixing stateful servers with load balancing produces this classic bug.
Sticky sessions, also called session affinity, pin a client to one backend for the life of their session. Two mechanisms dominate:
Sticky sessions drag back the exact problem horizontal scaling removed. One specific user now depends on one specific server.
When that server crashes, every user pinned to it loses their session. Load skews too, since a handful of very active users can bury one machine while others idle.
Treat stickiness as a short-term patch rather than a design choice. It works, but it quietly reintroduces a single point of failure per user.
Here is the part people discover the hard way. Affinity only forms as new sessions begin.
So a fresh server joins the fleet holding zero pinned users. Existing users stay glued to the older, already-busy machines.
Your new capacity helps far less than expected, at exactly the moment you need it most. Removing a server is just as awkward, because everyone pinned to it must migrate or lose their session outright.
The better answer removes the need for affinity entirely.
Rather than parking session data on one server, put it in a shared store every instance can read.
That keeps servers stateless. Statelessness is precisely what lets horizontal scaling and load balancing cooperate.
Any instance can then serve any request. Add servers, kill servers, replace servers, and nobody logged in ever notices.
In Spring Boot, the standard tool is Spring Session backed by Redis. Your code still reads and writes sessions through the normal Servlet API.
Underneath, Spring Session parks that data in Redis instead of local memory. Adding it is mostly configuration:
// build.gradle implementation 'org.springframework.session:spring-session-data-redis' implementation 'org.springframework.boot:spring-boot-starter-data-redis'
Then point the app at Redis:
# application.properties (Spring Boot 3.x and later) spring.session.store-type=redis spring.data.redis.host=localhost spring.data.redis.port=6379 # On Spring Boot 2.x these two keys were named # spring.redis.host and spring.redis.port
Watch those property names. Spring Boot 3.0 moved the Redis keys under spring.data.redis, and old tutorials still show the 2.x spelling.
Once this runs, a user can log in through one server and land anywhere next. Every instance reads the same Redis session, so nothing breaks and no stickiness is needed.
Remember the contrast. Sticky sessions treat the symptom by pinning users, while externalized state cures the cause by deleting server-side memory.
This is not free. Every session read becomes a network call rather than a memory lookup.
Inside one data centre that usually costs well under a millisecond. Most apps never feel it.
Redis also becomes a critical dependency. Deploy it with its own replication and failover, or you have simply moved the single point of failure somewhere new.
Almost always, that trade is worth taking. Interchangeable servers buy you more than a fraction of a millisecond ever costs.
One single point of failure still hides in this design.
It is the load balancer itself. When every request flows through one instance and that instance dies, your healthy backend fleet might as well be switched off.
Production systems run balancers in a pair or a cluster. An active-passive setup with a floating IP is common, where the address moves to the standby when the primary drops.
Cloud-managed balancers handle this for you across availability zones. Reaching four or five nines demands redundancy at every layer, and this layer is no exception.
Put bluntly, a single balancer can never be more available than the single server it replaced.
Health checks turn all of this into a self-healing system. The balancer probes each backend on a timer, and traffic stops flowing to anything that starts failing.
Spring Boot Actuator plugs straight into that. Expose the health endpoint and point your balancer at it:
# application.properties
management.endpoints.web.exposure.include=health
management.endpoint.health.probes.enabled=true
# The balancer then polls:
# GET /actuator/health
# A healthy instance answers:
# {"status":"UP"}Tuning matters more than people expect. Probe too slowly and dead servers keep taking traffic. Probe too aggressively and a briefly slow server flaps in and out of rotation.
Two more mechanisms reinforce redundancy at global scale:
Few application teams build either from scratch. Just know they exist, and ask one question whenever a design shows a load balancer: what happens if that exact box disappears?
A handful of misunderstandings surface again and again, in production incidents and interview rooms alike.
When an interview reaches load balancing, name Layer 4 or Layer 7 out loud and justify the pick. Multiple microservices behind different URL paths is an L7 requirement, so say so.
Expect a session follow-up whenever your design has logged-in users. Naming sticky sessions works as a first answer, and naming externalized storage as the long-term fix shows real depth.
Close by noting that the balancer needs redundancy too. Many candidates design a beautiful stateless fleet and leave one balancer as the weak point.
Mention client-side balancing for internal calls as well. Naming both models, and explaining when each fits, usually signals senior-level thinking here.
Let us tie the ideas into one small, concrete setup you could run today.
Say we run three copies of a Spring Boot storefront. Each listens on port 8080 on its own private address.
NGINX sits in front as our server-side Layer 7 balancer. Our goals are ordinary ones:
upstream storefront {
least_conn;
server 10.0.1.11:8080 max_fails=3 fail_timeout=10s;
server 10.0.1.12:8080 max_fails=3 fail_timeout=10s;
server 10.0.1.13:8080 max_fails=3 fail_timeout=10s;
}
upstream orders {
server 10.0.2.21:8080;
server 10.0.2.22:8080;
}
server {
listen 80;
location /api/orders/ {
proxy_pass http://orders; # L7 path-based routing
}
location / {
proxy_pass http://storefront;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}Read the pieces one at a time. The least_conn line picks our algorithm from section 6.
Each max_fails and fail_timeout pair defines a passive health check. Three failures inside ten seconds, and NGINX benches that server for ten seconds.
The two location blocks are Layer 7 at work. Only a balancer that reads the URL path could split traffic this way, which is exactly what section 5.3 described.
One accuracy note worth knowing. Open-source NGINX ships passive health checks only, as shown here. Active probing against /actuator/health requires NGINX Plus, HAProxy, or a cloud balancer.
Now kill the instance at 10.0.1.12 and watch what unfolds:
Notice what did not happen. Nobody got logged out, because sessions live in Redis rather than local memory.
Had we relied on sticky sessions instead, every user pinned to that instance would have lost their cart. That single difference is the whole argument of sections 7 and 8.
[IMAGE PLACEHOLDER: diagram of the request chain, showing GeoDNS choosing a region, an edge L7 balancer choosing a storefront server, and the storefront calling inventory via client-side balancing]
| Term | Meaning in one line |
|---|---|
| Load balancer | Spreads incoming traffic across many servers |
| Layer 4 (L4) | Routes on IP and port only, very fast |
| Layer 7 (L7) | Routes on full request content, more flexible |
| Round robin | Cycles through servers in order |
| Least connections | Sends the next request to the least busy server |
| Consistent hashing | Ring-based routing that minimises reshuffling |
| Client-side balancing | The caller picks a healthy instance itself, with no shared box |
| Service registry | A directory of live instances, such as Eureka or Consul |
| Service mesh | Moves service-to-service concerns into sidecar proxies, such as Istio |
| Sticky session | Pins a client to the same backend server |
| Session affinity | Another name for sticky sessions |
| Externalized session | Session data lives in a shared store like Redis |
| Health check | A periodic probe that removes unhealthy servers |
| Anycast | One IP advertised from many sites, routed to the nearest |
A: Three, sorted by where the routing decision happens. DNS-based balancing picks a region during name lookup. Server-side balancing sits in the request path and picks a server. Client-side balancing puts the choice inside the caller itself. Hardware, software, and cloud balancers are not a fourth type, they are three ways to build the server-side one.
A: A Layer 4 balancer works at the transport layer and sees only IP addresses and ports, which makes it very fast and cheap on CPU. A Layer 7 balancer works at the application layer and reads the whole HTTP request, so it can route by path, host, or header and terminate SSL. Pay for L7 when routing depends on content, and stay on L4 when it does not.
A: No, and this is the most common misreading of the name. Client here means whoever makes the request, which is usually another backend service, sometimes a mobile app or an SDK. Browsers stay out of it because they would need your internal addresses and your service registry, and because you cannot trust code running on a stranger’s machine.
A: Sticky sessions pin one client to one backend for the life of a session, usually through a cookie or the caller’s IP. They fix the logged-out bug caused by in-memory sessions. The catch is that they restore a single point of failure per user, skew load toward busy servers, and starve freshly autoscaled instances that hold no pinned users.
A: Move session state off the server into a shared store. In Spring Boot, add Spring Session with Redis and set spring.session.store-type to redis. Every instance then reads the same session, so any server can serve any request. Note that Spring Boot 3.0 renamed the connection keys to spring.data.redis.host and spring.data.redis.port.
A: Match it to your traffic. Round robin suits servers of equal capacity handling similar requests, and weighted round robin handles uneven hardware. Least connections wins when request durations vary widely. Consistent hashing fits caches and sharded stores, since adding a server reshuffles only a slice of keys rather than everything.
A: Everything behind it goes dark, no matter how healthy those servers are. Production setups run balancers as an active-passive pair with a floating IP, or as a cluster. Cloud-managed balancers spread themselves across availability zones automatically. DNS failover and anycast routing add another layer of protection above that.
A: Because browsers, operating systems, and intermediate resolvers all cache DNS answers for the record’s time-to-live. Until those caches expire, clients keep using the old address even after you change the record. Lowering the time-to-live speeds up failover but multiplies query volume against your DNS infrastructure.
A: They form a pair with separate jobs. Health checks decide which servers are eligible, and the algorithm decides which eligible server takes the next request. Tuning only the algorithm cannot help if health checks are too slow to notice a dead server, so treat their thresholds as part of the design.
A: Only large multi-region systems use all three. A single-region app runs fine with one server-side balancer at the edge. Add DNS-based balancing when you serve more than one region, and add client-side balancing when internal services call each other often enough that an extra network hop starts to hurt.
Let us wrap up what we covered. Load balancing turns a pile of separate servers into one reliable service.
Three types share the work. DNS picks a region, a server-side balancer picks a server inside it, and client-side balancing picks an instance for service-to-service calls.
Among server-side balancers, the layer decides what you can see. Layer 4 gives you speed, Layer 7 gives you insight into the request, and most systems end up running both in tiers.
Sticky sessions fix an urgent problem and hand you a fragile one. Externalizing state into Redis is the durable answer, since it keeps every instance interchangeable.
One last habit to carry with you. Whenever you draw a load balancer, ask immediately whether that box is redundant. A system is only as available as its weakest single point of failure, and the balancer is the easiest one to forget.