Home Java Spring Cloud Ribbon: Client-Side Load Balancing Done Right
Advanced 7 min · July 14, 2026

Spring Cloud Ribbon: Client-Side Load Balancing Done Right

Master client-side load balancing with Netflix Ribbon in Spring Cloud.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • Java 8 or later
  • Spring Boot 2.x knowledge
  • Basic understanding of microservices and REST APIs
  • Familiarity with Spring Cloud and Eureka (recommended)
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 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.
✦ Definition~90s read
What is Spring Cloud Rest Client with Netflix Ribbon?

Spring Cloud Ribbon is a client-side load balancer that distributes outgoing requests across multiple service instances, integrating with Eureka for dynamic server lists and providing built-in retry and fault tolerance.

Imagine you're at a busy food court with multiple counters of the same restaurant.
Plain-English First

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.

```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.

RibbonRestTemplateConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RibbonRestTemplateConfig {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
🔥Important: Ribbon vs Spring Cloud LoadBalancer
📊 Production Insight
By default, Ribbon uses lazy initialization. The first request to a service will be slow because it needs to fetch the server list from Eureka. Enable eager loading via ribbon.eager-load.enabled=true to avoid this.
🎯 Key Takeaway
Ribbon provides client-side load balancing via the @LoadBalanced RestTemplate. It's deprecated but still widely used.

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.

``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.

application.ymlJAVA
1
2
3
4
5
6
7
8
9
10
11
payment-service:
  ribbon:
    ServerListRefreshInterval: 5000
    NFLoadBalancerPingInterval: 5000
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule
    MaxHttpConnectionsPerHost: 100
    ReadTimeout: 3000
    ConnectTimeout: 1000
    OkToRetryOnAllOperations: true
    MaxAutoRetries: 1
    MaxAutoRetriesNextServer: 1
⚠ Watch Out: Thread Pool Exhaustion
🎯 Key Takeaway
Tune ServerListRefreshInterval, timeouts, and connection limits. Defaults are rarely optimal for production.

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.

``yaml my-service: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule ``

CustomLoadBalancerRule.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.Server;
import java.util.List;
import java.util.Random;

public class CustomLoadBalancerRule extends AbstractLoadBalancerRule {

    private final Random random = new Random();

    @Override
    public Server choose(Object key) {
        List<Server> servers = getLoadBalancer().getAllServers();
        if (servers.isEmpty()) {
            return null;
        }
        // Custom logic: prefer servers with even index (just for illustration)
        List<Server> evenServers = servers.stream()
                .filter(s -> servers.indexOf(s) % 2 == 0)
                .toList();
        if (evenServers.isEmpty()) {
            return servers.get(random.nextInt(servers.size()));
        }
        return evenServers.get(random.nextInt(evenServers.size()));
    }

    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
        // No-op
    }
}
💡Rule Selection Cheat Sheet
📊 Production Insight
If you use WeightedResponseTimeRule, be aware that it uses an exponential moving average of response times. A sudden spike in latency (e.g., due to a GC pause) can temporarily reduce the weight of a healthy server. This is usually fine, but monitor for oscillations.
🎯 Key Takeaway
WeightedResponseTimeRule is the best default for production. Custom rules are possible but rarely needed.

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.

``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.

EurekaClientConfig.javaJAVA
1
2
3
4
5
6
7
8
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableEurekaClient
public class EurekaClientConfig {
    // This configuration enables the Eureka client
}
🔥What the Official Docs Won't Tell You
🎯 Key Takeaway
Align Ribbon's refresh interval with Eureka's registry fetch interval to minimize stale server lists.

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.

HystrixRibbonConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
public class PaymentService {

    @Autowired
    private RestTemplate restTemplate;

    @HystrixCommand(fallbackMethod = "fallback")
    public String processPayment(String paymentId) {
        return restTemplate.postForObject(
                "http://payment-service/api/payments/process",
                paymentId,
                String.class);
    }

    public String fallback(String paymentId) {
        return "Payment processing is temporarily unavailable. Please try again later.";
    }
}
⚠ Retry Non-Idempotent Operations at Your Own Risk
🎯 Key Takeaway
Retry can improve reliability but must be used carefully with non-idempotent operations. Combine with Hystrix for circuit breaking.

What the Official Docs Won't Tell You

Let's cut through the sanitized documentation. Here are the real-world gotchas I've seen:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.

🎯 Key Takeaway
Ribbon has many subtle integration issues. Test thoroughly and monitor metrics.

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.

``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.

RibbonDebugController.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RibbonDebugController {

    @Autowired
    private LoadBalancerClient loadBalancerClient;

    @GetMapping("/ribbon-status")
    public String getStatus() {
        // You can inspect the load balancer state here
        return "Check actuator endpoints for details.";
    }
}
💡Quick Debug Tip
🎯 Key Takeaway
Use actuator endpoints and DEBUG logging to monitor Ribbon. Compare server lists with Eureka regularly.
● Production incidentPOST-MORTEMseverity: high

The Stale Server List Disaster

Symptom
Users reported random 503 errors when initiating payments. The error rate spiked to 15% during peak hours.
Assumption
The developer assumed the payment service was overloaded and added more instances, but the errors persisted.
Root cause
Ribbon's ServerListRefreshInterval was set to 30000ms (30 seconds), but the Eureka server was marking instances as DOWN within 15 seconds of a health check failure. The client's stale server list included dead instances, causing requests to fail.
Fix
Reduced ServerListRefreshInterval to 5000ms and set NFLoadBalancerPingInterval to 5 seconds, ensuring the client's server list matched Eureka's state within a few seconds.
Key lesson
  • 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.
Production debug guideSymptom to Action5 entries
Symptom · 01
Random 503 errors on service calls
Fix
Check Ribbon's server list via actuator: /actuator/ribbonclient?name=service-id. Verify Eureka instance status.
Symptom · 02
Uneven load distribution
Fix
Check the load balancing rule (e.g., RoundRobin, WeightedResponseTime). Ensure instances have similar response times.
Symptom · 03
High latency on first request
Fix
Ribbon lazy-loads the server list. Set eager loading: ribbon.eager-load.enabled=true.
Symptom · 04
Thread pool exhaustion
Fix
Check HttpClient or OkHttp thread pool size. Ribbon uses a fixed thread pool for each service; increase if needed.
Symptom · 05
Stale server list after scaling down
Fix
Reduce ServerListRefreshInterval. Also check if Eureka's eviction interval is faster than Ribbon's refresh.
★ Quick Debug Cheat SheetCommon Ribbon issues and immediate actions.
503 Service Unavailable
Immediate action
Check Ribbon's server list via actuator
Commands
curl /actuator/ribbonclient?name=my-service
curl /actuator/health
Fix now
Reduce ServerListRefreshInterval to 5s
Load imbalance+
Immediate action
Check load balancing rule
Commands
Check logs for 'Using load balancer: RoundRobinRule'
Verify instance response times
Fix now
Switch to WeightedResponseTimeRule
Slow first request+
Immediate action
Enable eager load
Commands
Check application logs for server list load time
Set ribbon.eager-load.enabled=true
Fix now
Add property and restart
Thread pool full+
Immediate action
Check thread pool size
Commands
jstack or thread dump
Look for 'Ribbon-loadbalancer' threads
Fix now
Increase ribbon.MaxHttpConnectionsPerHost
FeatureRibbonSpring Cloud LoadBalancer
TypeBlocking (synchronous)Reactive (non-blocking)
Actively maintainedNo (deprecated)Yes
Integration with EurekaNativeVia DiscoveryClient
Retry supportBuilt-inVia Spring Retry
Custom rulesExtend AbstractLoadBalancerRuleImplement ReactorLoadBalancer
WebFlux supportNoYes
Thread poolDedicated per serviceReactive (no thread pool)
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
RibbonRestTemplateConfig.java@ConfigurationWhat Is Ribbon and Why Should You Care?
application.ymlpayment-service:Configuring Ribbon
CustomLoadBalancerRule.javapublic class CustomLoadBalancerRule extends AbstractLoadBalancerRule {Load Balancing Rules
EurekaClientConfig.java@ConfigurationIntegrating Ribbon with Eureka
HystrixRibbonConfig.java@ServiceRetry and Fault Tolerance with Ribbon
RibbonDebugController.java@RestControllerMonitoring and Debugging Ribbon in Production

Key takeaways

1
Ribbon is a client-side load balancer that integrates with Eureka for dynamic service discovery. It's deprecated but still widely used in legacy systems.
2
Configure ServerListRefreshInterval, timeouts, and connection limits for production. Defaults are often too conservative.
3
Use WeightedResponseTimeRule for adaptive load balancing. Avoid RoundRobinRule for heterogeneous environments.
4
Enable eager loading and align refresh intervals with Eureka to minimize stale server lists.
5
Be cautious with retries on non-idempotent operations. Use Hystrix for circuit breaking.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain how client-side load balancing differs from server-side load bal...
Q02JUNIOR
What is the purpose of the @LoadBalanced annotation on a RestTemplate be...
Q03SENIOR
Describe a scenario where Ribbon's default configuration could cause a p...
Q01 of 03SENIOR

Explain how client-side load balancing differs from server-side load balancing. When would you use each?

ANSWER
Client-side load balancing (like Ribbon) runs inside the client application, distributing requests across service instances based on rules and real-time metrics. It's useful for microservices where you want to avoid a single point of failure (the load balancer) and need fine-grained control. Server-side load balancing (like HAProxy or AWS ELB) is a separate component that receives all requests and forwards them. It's simpler to manage and works well for traditional web apps. In microservices, client-side is often preferred for its decentralization and ability to use service discovery.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
Is Ribbon still supported in Spring Cloud?
02
How do I enable eager loading for Ribbon?
03
What's the difference between Ribbon and Spring Cloud LoadBalancer?
04
Can I use Ribbon without Eureka?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Spring Cloud. Mark it forged?

7 min read · try the examples if you haven't

Previous
Netflix Archaius with Spring Cloud: Dynamic Configuration Management
23 / 34 · Spring Cloud
Next
Integration Testing with Spring Cloud Netflix and Feign Clients