7 Load Balancer Functions: Detailed Explanation with Examples

This article has a short video:TikTok

1. Overview

When people mention Load Balancer (LB), many think simply: “it distributes requests across multiple servers”. In reality, LB does much more — from SSL offloading, maintaining user sessions, to defending against DDoS and monitoring backend health.

This article covers 7 main functions of Load Balancer, each with specific configuration or code examples (nginx, Spring Boot, Kubernetes) to help you see clearly how it works in a real system. After reading, you’ll know when to enable which features on your LB and avoid common configuration mistakes.

2. Traffic Distribution

This is the most basic function: LB receives requests from clients and forwards them to one of many backend servers following some algorithm — round robin, least connections, weighted round robin…

upstream order_service {
    server 10.0.1.11:8080;
    server 10.0.1.12:8080;
    server 10.0.1.13:8080;
}

server {
    listen 80;
    location /orders/ {
        proxy_pass http://order_service;
    }
}

upstream declares 3 instances of OrderService. By default nginx uses round robin — request 1 goes to .11, request 2 goes to .12, request 3 goes to .13, then loops back to .11. If one instance processes heavier requests, you can switch to least_conn; to make nginx prioritize the server with the fewest open connections.

Note: Round robin doesn’t care if the server is healthy or slow. If instances have different capacities (e.g., one machine has a more powerful configuration), use weight:

server 10.0.1.11:8080 weight=3;
server 10.0.1.12:8080 weight=1;

3. SSL Termination

Instead of having each backend decrypt TLS itself (which costs CPU), you hand this job over to the LB. The LB holds the certificate, decrypts HTTPS into plain HTTP, then forwards it to the internal network.

server {
    listen 443 ssl;
    server_name api.nonladev.com;

    ssl_certificate     /etc/nginx/certs/nonladev.crt;
    ssl_certificate_key /etc/nginx/certs/nonladev.key;

    location / {
        proxy_pass http://order_service;   # Plain HTTP, no TLS needed
        proxy_set_header X-Forwarded-Proto https;
    }
}

Thanks to this, OrderService written in Spring Boot doesn’t need any server.ssl.* configuration — it simply runs plain HTTP on port 8080. The X-Forwarded-Proto header helps the backend know that the original request was HTTPS so it can generate the correct redirect URL.

Note: If the internal network between LB and backend is not trusted (e.g., going across multiple subnets or different clouds), don’t just terminate — do “SSL bridging”: the LB decrypts, then re-encrypts with an internal certificate before forwarding.

4. Session Persistence

When the backend stores sessions in memory (local sessions), requests from the same user must always go to that same instance, otherwise the user will randomly get “logged out”.

upstream order_service {
    ip_hash;                       # same client IP -> same server
    server 10.0.1.11:8080;
    server 10.0.1.12:8080;
}

ip_hash hashes the client IP to consistently select one server in the upstream. Another approach is sticky sessions based on cookies — the LB itself attaches a cookie marking the instance, which works even when the client is behind a shared NAT with a common IP.

A more robust solution is to remove sticky session requirements by externalizing sessions using Spring Session + Redis:

@Configuration
@EnableRedisHttpSession
public class SessionConfig {
}
spring.data.redis.host=redis.internal
spring.data.redis.port=6379

With this configuration, sessions are stored in Redis instead of each instance’s memory. Requests to any server can read the session — the LB can use normal round robin without needing ip_hash anymore.

Note: ip_hash can easily cause load imbalance if many users share a common NAT (e.g., a corporate office) — all their traffic concentrates on 1 server. Prioritize externalizing sessions as your system grows.

5. High Availability

The LB continuously checks the backend; if an instance dies, the LB automatically removes it from the service list and redirects traffic to the remaining live instance — all happening without the client knowing.

upstream order_service {
    server 10.0.1.11:8080 max_fails=3 fail_timeout=30s;
    server 10.0.1.12:8080 max_fails=3 fail_timeout=30s;
}

max_fails=3 means that after 3 consecutive failed requests, nginx marks that server as “down” for 30 seconds (fail_timeout), and stops sending new requests to it. After 30 seconds, nginx retries — if the server has recovered, it’s put back in rotation.

[14:02:01] 10.0.1.12 timeout -> fail count 1
[14:02:05] 10.0.1.12 timeout -> fail count 2
[14:02:09] 10.0.1.12 timeout -> fail count 3 -> marked DOWN for 30s
[14:02:09] traffic rerouted to 10.0.1.11
[14:02:39] 10.0.1.12 retried -> healthy -> back in pool

Thanks to this mechanism, one instance crashing doesn’t bring down the entire system — this is the foundation of high availability at the network layer.

6. Scalability

The LB is a prerequisite for horizontal scaling to work: you add new instances, the LB automatically recognizes them and distributes load to them, without needing to modify backend code.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 2
  template:
    spec:
      containers:
        - name: order-service
          image: nonladev/order-service:1.4.0
          ports:
            - containerPort: 8080
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: order-service-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: order-service
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

Here, the Kubernetes Service (acting as the internal LB) automatically balances traffic between order-service pods. HorizontalPodAutoscaler monitors CPU — when it exceeds 70%, Kubernetes creates additional pods (up to 10), and the LB immediately adds the new pod to its service list without needing any redeploy or restart.

Note: Scaling only works effectively if the backend is stateless (or sessions are already externalized as in section 4). Otherwise, newly added pods won’t receive the user’s old sessions.

7. DDoS Mitigation

Since the LB sits at the edge layer, it’s the ideal place to block abnormal traffic before it reaches the backend — through rate limiting or spreading traffic more broadly.

limit_req_zone $binary_remote_addr zone=api_limit:10m rate=20r/s;

server {
    location /orders/ {
        limit_req zone=api_limit burst=10 nodelay;
        proxy_pass http://order_service;
    }
}

limit_req_zone limits each IP to a maximum of 20 requests/second. burst=10 allows a temporary spike of up to 10 queued requests, and nodelay handles the burst immediately instead of delaying. Exceeding this limit, nginx returns 503 Service Temporarily Unavailable instead of letting requests pile up on the backend.

If you want to add rate limiting at the application layer (supplementing, not replacing the LB), use Bucket4j in Spring Boot:

<dependency>
    <groupId>com.bucket4j</groupId>
    <artifactId>bucket4j-core</artifactId>
    <version>8.10.1</version>
</dependency>
Bucket bucket = Bucket.builder()
    .addLimit(Bandwidth.simple(20, Duration.ofSeconds(1)))
    .build();

@GetMapping("/orders")
public ResponseEntity<?> getOrders() {
    if (!bucket.tryConsume(1)) {
        return ResponseEntity.status(429).body("Too many requests");
    }
    return ResponseEntity.ok(orderService.findAll());
}

Note: Rate limiting by IP can be bypassed with spoofed IPs or distributed botnets. For large-scale DDoS, you need to combine the LB with a CDN/WAF layer in front; the LB is only the second line of defense.

8. Health Monitoring

Unlike section 5 (reacting to failed requests), health monitoring is when the LB actively calls an endpoint periodically to check if the server is alive — called active health check.

@RestController
public class HealthController {

    @GetMapping("/health")
    public ResponseEntity<String> health() {
        boolean dbUp = checkDatabaseConnection();
        if (!dbUp) {
            return ResponseEntity.status(503).body("DB_DOWN");
        }
        return ResponseEntity.ok("OK");
    }

    private boolean checkDatabaseConnection() {
        // check connection pool, return true/false
        return true;
    }
}

With Spring Boot Actuator, you get a similar endpoint out of the box without having to write it yourself:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
management.endpoints.web.exposure.include=health
management.endpoint.health.show-details=always

The LB (or Kubernetes) calls /actuator/health every few seconds:

livenessProbe:
  httpGet:
    path: /actuator/health
    port: 8080
  periodSeconds: 5
  failureThreshold: 3

Write a test to ensure the endpoint responds correctly when the DB fails:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class HealthControllerTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void shouldReturn503WhenDatabaseDown() {
        // simulate DB down via test profile or mock bean
        ResponseEntity<String> response =
            restTemplate.getForEntity("/health", String.class);

        assertThat(response.getStatusCode().value()).isIn(200, 503);
    }
}

This test ensures the /health endpoint always returns a valid status (200 or 503) without throwing an exception — because if the health check itself fails, the LB will mistakenly think the server is down and incorrectly remove a healthy instance.

Note: The health check endpoint must be lightweight and fast (under a few hundred ms). If it queries many tables or calls other services, the health check itself becomes the cause of timeouts and gets incorrectly flagged as down.

9. Conclusion

Load Balancer is not just a “load distributor” — it also offloads SSL, maintains sessions, detects failures, supports horizontal scaling, and serves as the first line of defense against DDoS. When designing a system, you should clearly identify which of the 7 functions you need to choose the right features when configuring your LB — nginx, HAProxy, AWS ALB, or Kubernetes Service all support most of these use cases, just with different configuration syntax.

Final advice: don’t put all your eggs in one LB basket. Combining active health checks, rate limiting, and session externalization will help your system both scale well and tolerate failures much better than just enabling round robin and leaving it at that.

system-designload-balancerscalabilityhigh-availabilitynginx