Spring Cloud Ribbon: Client-Side Load Balancing Done Right
Master client-side load balancing with Netflix Ribbon in Spring Cloud.
20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.
- ✓Java 8 or later
- ✓Spring Boot 2.x knowledge
- ✓Basic understanding of microservices and REST APIs
- ✓Familiarity with Spring Cloud and Eureka (recommended)
- Ribbon enables client-side load balancing for microservices, distributing requests across service instances.
- It integrates with Eureka for dynamic service discovery and provides built-in retry and fault tolerance.
- Key components: load balancer, rule, ping, and server list.
- Common gotchas include incorrect configuration, stale caches, and thread pool exhaustion.
- Modern alternatives like Spring Cloud LoadBalancer (Reactive) exist, but Ribbon is still widely used in legacy systems.
Imagine you're at a busy food court with multiple counters of the same restaurant. Instead of having a manager (server-side load balancer) tell you which counter to go to, you (the client) pick the one with the shortest line. Ribbon is like that manager inside your app, deciding which server instance to call based on rules like round-robin or availability.
You've built a microservice that talks to another service. It works perfectly in development with one instance. Then you deploy to production with three instances, and suddenly you're getting timeouts, uneven load distribution, and the occasional 503. Welcome to the real world of distributed systems.
Client-side load balancing is one of those patterns that seems simple on paper but bites you in production if you don't understand the nuances. Netflix Ribbon was the de facto standard for years in the Spring Cloud ecosystem, and while it's now in maintenance mode (Spring Cloud 2020.0 deprecated it in favor of Spring Cloud LoadBalancer), thousands of production systems still run on it. If you're maintaining a legacy microservices stack or working on a migration, you need to know Ribbon inside out.
I've spent years debugging production incidents where Ribbon was the culprit — from stale server lists causing all traffic to hit a dead instance, to thread pool exhaustion from misconfigured timeouts. This article will give you the hard-won wisdom that the official docs gloss over. We'll cover how Ribbon works, how to configure it properly, common pitfalls, and a real production incident that will make you think twice about default settings.
What Is Ribbon and Why Should You Care?
Ribbon is a client-side load balancer that gives your microservice the ability to choose which instance of a remote service to call. Unlike server-side load balancers (like HAProxy or AWS ELB), Ribbon runs inside your application process. This means it can make smarter decisions based on real-time metrics like response times, active connections, and even zone affinity.
Let's be clear: Ribbon is not the shiny new toy. It's a battle-hardened library from the Netflix OSS stack that has been in production at massive scale for years. But it's also in maintenance mode since Spring Cloud 2020.0. If you're starting a new project, you should use Spring Cloud LoadBalancer (which is reactive and non-blocking). But if you're maintaining a legacy system, you need to know Ribbon's quirks.
The core abstraction is the ILoadBalancer interface. It maintains a list of servers, a rule for choosing one, and a ping mechanism to check server health. By default, Ribbon uses a RoundRobinRule, but you can plug in custom rules for weighted response times, availability filtering, or even random selection.
Here's a simple REST client using Ribbon with RestTemplate:
```java @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); }
// Usage String result = restTemplate.getForObject("http://payment-service/api/pay", String.class); ```
The @LoadBalanced annotation tells Spring Cloud to intercept the RestTemplate and wrap it with a RibbonLoadBalancerClient. The URL uses the service name (payment-service) which is resolved by Eureka to actual instances.
Configuring Ribbon: Properties That Matter
Ribbon configuration is done via application properties. The most important ones are:
- NIWSServerListClassName: The class that provides the list of servers. For Eureka, it's com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList.
- ServerListRefreshInterval: How often (in ms) the server list is refreshed from the discovery service. Default is 30000 (30 seconds).
- NFLoadBalancerPingInterval: How often (in ms) the ping mechanism checks server health. Default is 30000.
- NFLoadBalancerRuleClassName: The load balancing rule. Common options: RoundRobinRule, WeightedResponseTimeRule, AvailabilityFilteringRule.
- MaxHttpConnectionsPerHost: Maximum number of connections per host. Default is 50.
- ReadTimeout: Socket read timeout in ms. Default is 5000.
- ConnectTimeout: Socket connect timeout in ms. Default is 2000.
Here's a configuration for a service called 'payment-service':
``yaml payment-service: ribbon: ServerListRefreshInterval: 5000 NFLoadBalancerPingInterval: 5000 NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule MaxHttpConnectionsPerHost: 100 ReadTimeout: 3000 ConnectTimeout: 1000 ``
Production Insight: The default 30-second refresh interval is almost always too long. If your Eureka server evicts instances faster than 30 seconds, your client will have stale servers. I've seen this cause cascading failures where a single instance goes down but clients keep hitting it for up to 30 seconds. Set both intervals to 5 seconds or less.
Also, be careful with timeouts. A common mistake is setting ReadTimeout too high, which can cause thread pool exhaustion under load. A good starting point is 3 seconds for read and 1 second for connect, then adjust based on your service's SLA.
Load Balancing Rules: Choosing the Right Strategy
Ribbon provides several built-in load balancing rules. The default is RoundRobinRule, which cycles through servers in order. This is simple but doesn't account for server load or response times.
- RoundRobinRule: Simple round-robin. Good for homogeneous instances with similar capacity.
- WeightedResponseTimeRule: Assigns weights based on average response times. Faster servers get more traffic. This is my go-to for most production systems.
- AvailabilityFilteringRule: Skips servers that are marked as circuit-tripped or have too many active connections. Useful when you have slow or failing instances.
- ZoneAvoidanceRule: Prioritizes servers in the same zone (AWS availability zone) to reduce latency and cost.
- RandomRule: Random selection. Useful for testing or when you want to distribute load uniformly.
Production Opinion: Use WeightedResponseTimeRule unless you have a specific reason not to. It adapts to changing server performance automatically. RoundRobinRule can cause overload on slower instances if they're part of the rotation.
You can also write a custom rule by extending AbstractLoadBalancerRule. For example, you might want a rule that prefers servers with lower CPU usage. But be careful: custom rules can introduce complexity and bugs. Stick with built-in rules unless you really need something special.
Here's how to set the rule in properties:
``yaml my-service: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule ``
Integrating Ribbon with Eureka
Ribbon works best with a service discovery system like Eureka. When you include spring-cloud-starter-netflix-eureka-client, Ribbon automatically uses DiscoveryEnabledNIWSServerList to fetch the server list from Eureka.
The integration is seamless: you just use the service name in your RestTemplate URL, and Ribbon resolves it to actual instances. But there are gotchas.
Eureka's Self-Preservation Mode: During network partitions, Eureka enters self-preservation mode and stops evicting instances. This can cause Ribbon to have stale servers for longer. If you're in a cloud environment with frequent network issues, consider increasing Ribbon's ping interval to match Eureka's eviction delay.
Zone Affinity: Eureka assigns each instance a metadata map that can include zone information. Ribbon's ZoneAvoidanceRule uses this to prefer servers in the same zone. This is critical for reducing cross-zone latency and data transfer costs in AWS. Configure your instances with proper zone metadata.
Multiple Discovery Clients: If you have multiple discovery clients (e.g., Eureka and Consul), Ribbon can be configured to use a specific one. But I've seen this cause confusion. Stick with one discovery service per application.
Here's a typical Eureka client configuration:
``yaml eureka: client: serviceUrl: defaultZone: http://eureka-server:8761/eureka/ fetchRegistry: true registerWithEureka: true registryFetchIntervalSeconds: 5 ``
The registryFetchIntervalSeconds controls how often the Eureka client fetches the registry. This is separate from Ribbon's ServerListRefreshInterval. Both need to be aligned to avoid stale data.
Retry and Fault Tolerance with Ribbon
One of Ribbon's killer features is built-in retry. When a request fails, Ribbon can automatically retry on the same server or try a different server. This is configured via:
- OkToRetryOnAllOperations: Whether to retry on all HTTP methods (GET, POST, etc.). Default is false (only GET). Be careful: retrying non-idempotent operations like POST can cause duplicate side effects.
- MaxAutoRetries: Number of retries on the same server. Default is 0.
- MaxAutoRetriesNextServer: Number of times to try a different server. Default is 1.
Here's a configuration that retries once on the same server and once on a different server for all operations:
``yaml payment-service: ribbon: OkToRetryOnAllOperations: true MaxAutoRetries: 1 MaxAutoRetriesNextServer: 1 ``
Production Warning: I once saw a payment system where OkToRetryOnAllOperations was true for a POST endpoint that created charges. A network timeout caused the request to be retried on another server, resulting in duplicate charges. The fix was to either make the endpoint idempotent or disable retries for POST.
Ribbon also integrates with Hystrix for circuit breaking. When you use @HystrixCommand, Ribbon's load balancer is aware of the circuit breaker state and will skip servers that are open. This is a powerful combination for resilience.
But remember: retries can mask transient failures, but they can also amplify load during cascading failures. Use them judiciously.
What the Official Docs Won't Tell You
Let's cut through the sanitized documentation. Here are the real-world gotchas I've seen:
- Ribbon and Spring Cloud Gateway Don't Play Well: If you're using Spring Cloud Gateway (reactive), Ribbon is blocking and will cause thread pool issues. Use Spring Cloud LoadBalancer instead. I've seen teams waste days trying to make Ribbon work with WebFlux.
- ServerListRefreshInterval vs Eureka's eviction: As mentioned, if Eureka evicts instances faster than Ribbon refreshes, you'll hit dead servers. But there's another twist: when Eureka is in self-preservation mode, it doesn't evict at all. Your Ribbon client will keep hitting instances that are actually down. Monitor Eureka's self-preservation status.
- Connection Pooling: Ribbon uses Apache HttpClient or OkHttp under the hood. The default connection pool is often too small. I've seen production incidents where the pool exhausted because of slow responses, causing cascading failures. Increase MaxHttpConnectionsPerHost and monitor connection pool metrics.
- Eager Loading is Not Enough: Even with eager loading enabled, the first request after a server list refresh can be slow because the new servers aren't in the connection pool. Consider pre-warming connections by making a health check call when the application starts.
- Ribbon with Feign: Feign uses Ribbon by default. But Feign's configuration can override Ribbon's settings in subtle ways. Always test your configuration end-to-end.
- Deprecation Anxiety: Ribbon is deprecated since Spring Cloud 2020.0. But many organizations are still on older versions. If you're planning a migration, start by isolating Ribbon configuration so you can swap it out later. Spring Cloud LoadBalancer is the way forward.
Production Incident Bonus: I once debugged a case where Ribbon was sending all traffic to one instance even though there were 10 healthy ones. The culprit was a custom load balancing rule that had a bug: it was using instance IDs that weren't unique across deployments. Always test your custom rules with multiple instances.
Monitoring and Debugging Ribbon in Production
Ribbon exposes several metrics and health indicators that are invaluable for debugging:
- /actuator/ribbonclient: Shows the server list for each service. Use this to verify that Ribbon has the correct instances.
- /actuator/metrics/ribbon.*: Ribbon exposes metrics like active connections, server list size, and request counts.
- Logging: Set com.netflix.loadbalancer to DEBUG to see server list refreshes, ping results, and load balancing decisions.
Here's how to enable Ribbon logging:
``yaml logging: level: com.netflix.loadbalancer: DEBUG ``
Production Debugging Steps: 1. When you see 503s, check /actuator/ribbonclient?name=service-id to see the current server list. 2. Compare with Eureka's dashboard. If they differ, check refresh intervals. 3. Look at /actuator/metrics/ribbon.client.<service>.activeConnections to see if connections are maxed out. 4. Use a thread dump to see if threads are stuck on Ribbon operations.
I've also found it useful to expose a custom endpoint that shows the load balancer state:
```java @RestController public class RibbonDebugController {
@Autowired private LoadBalancerClient loadBalancerClient;
@GetMapping("/ribbon-status") public Map<String, Object> getStatus() { // Custom logic to inspect Ribbon's state return Map.of("status", "ok"); } } ```
But honestly, the actuator endpoints are usually enough.
The Stale Server List Disaster
- Always align Ribbon's refresh interval with your Eureka health check intervals.
- Never assume adding more instances solves load balancing issues; check the client's server list first.
- Enable Ribbon logging (com.netflix.loadbalancer: DEBUG) to see server list updates in real-time.
- Use the /actuator/health endpoint to verify Ribbon's server list during incidents.
- Consider migrating to Spring Cloud LoadBalancer for reactive, non-blocking load balancing in new projects.
curl /actuator/ribbonclient?name=my-servicecurl /actuator/health| File | Command / Code | Purpose |
|---|---|---|
| RibbonRestTemplateConfig.java | @Configuration | What Is Ribbon and Why Should You Care? |
| application.yml | payment-service: | Configuring Ribbon |
| CustomLoadBalancerRule.java | public class CustomLoadBalancerRule extends AbstractLoadBalancerRule { | Load Balancing Rules |
| EurekaClientConfig.java | @Configuration | Integrating Ribbon with Eureka |
| HystrixRibbonConfig.java | @Service | Retry and Fault Tolerance with Ribbon |
| RibbonDebugController.java | @RestController | Monitoring and Debugging Ribbon in Production |
Key takeaways
Interview Questions on This Topic
Explain how client-side load balancing differs from server-side load balancing. When would you use each?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.
That's Spring Cloud. Mark it forged?
7 min read · try the examples if you haven't