Why Does Feign’s 3‑Second Timeout Actually Take 10 Seconds? The Hidden Role of Ribbon
Feign may be set to a 3‑second timeout, but in Spring Cloud versions before 2020.0 the call is handled by Ribbon, whose default 5‑second read timeout and a single retry to another instance double the wait, producing a consistent 10‑second timeout; the article details this behavior and shows how to configure Ribbon, upgrade Spring Cloud, or use a circuit‑breaker to enforce the intended timeout.
Many developers configure Feign with a clear 3‑second connect and read timeout, expecting the request to fail after at most three seconds when a downstream service is slow or unavailable. In practice, monitoring often shows a stable timeout around 10 seconds.
Why Feign’s Timeout Is Ignored
In Spring Cloud versions prior to 2020.0 (corresponding to Spring Boot before 2.4), Feign does not send HTTP requests directly. Instead, it delegates to LoadBalancerFeignClient provided by Ribbon. You can verify this by running mvn dependency:tree | grep ribbon and checking for spring-cloud-starter-netflix-ribbon. If present, Ribbon is the component that actually performs the HTTP call.
Ribbon defines its own timeout defaults in DefaultClientConfigImpl:
public static final int DEFAULT_CONNECT_TIMEOUT = 2000;
public static final int DEFAULT_READ_TIMEOUT = 5000;The default read timeout of 5000 ms overrides the 3000 ms you set in Feign. However, this alone accounts for only 5 seconds. The remaining time comes from Ribbon’s default retry behavior.
Ribbon’s Default Retry Settings
Ribbon has two retry‑related parameters:
// Number of retries on the same instance (excluding the first attempt)
ribbon.MaxAutoRetries = 0
// Number of retries when switching to the next instance
ribbon.MaxAutoRetriesNextServer = 1
// MaxAutoRetriesNextServer = 1 means that after the first instance times out, Ribbon will automatically try another instance once.The total wait time can be calculated as:
TotalTime = ReadTimeout × (MaxAutoRetries + 1) × (MaxAutoRetriesNextServer + 1)Using the default values: 5000 × (0 + 1) × (1 + 1) = 10000 ms, which matches the observed 10‑second timeout. The first instance waits 5 seconds, then Ribbon retries the second instance for another 5 seconds.
How Ribbon Overrides Feign Configuration
Inspecting the source of FeignLoadBalancer shows that the timeout values are taken from Ribbon’s IClientConfig:
@Override
public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride) throws IOException {
Request.Options options;
if (configOverride != null) {
RibbonProperties override = RibbonProperties.from(configOverride);
options = new Request.Options(
override.connectTimeout(this.connectTimeout),
override.readTimeout(this.readTimeout));
} else {
options = new Request.Options(this.connectTimeout, this.readTimeout);
}
// ...
}Thus, this.connectTimeout and this.readTimeout come from Ribbon, not from Feign, making Feign’s timeout configuration ineffective. The effective configuration priority is:
Ribbon service‑level configuration
Ribbon global configuration
Ribbon default values
Feign configuration (ignored)
How to Fix the Issue
Option 1: Configure Ribbon Directly
ribbon:
ConnectTimeout: 3000
ReadTimeout: 3000
MaxAutoRetries: 0
MaxAutoRetriesNextServer: 0Setting MaxAutoRetriesNextServer to 0 removes the automatic retry, which is especially important for non‑idempotent POST requests.
For a specific service:
order-service:
ribbon:
ReadTimeout: 5000
MaxAutoRetries: 0
MaxAutoRetriesNextServer: 0Option 2: Upgrade Spring Cloud
Starting with Spring Cloud 2020.0, Ribbon is removed in favor of Spring Cloud LoadBalancer. After upgrading, Feign’s timeout settings become effective again:
feign:
client:
config:
default:
connectTimeout: 3000
readTimeout: 3000If upgrading is not feasible, continue using Ribbon but avoid configuring Feign timeouts.
Option 3: Add a Circuit‑Breaker Layer
When the underlying timeout hierarchy is unclear, wrap Feign with a circuit‑breaker such as Sentinel or Resilience4j to enforce a hard timeout and provide a fallback.
Verifying the Effective Configuration
Create a test endpoint that sleeps longer than any timeout:
@GetMapping("/slow")
public String slow() throws InterruptedException {
Thread.sleep(15000);
return "done";
}Call this endpoint via Feign and observe the elapsed time:
~3 seconds → Feign timeout is effective.
~5 seconds → Ribbon default timeout is in effect (retry disabled).
~10 seconds → Ribbon default timeout plus default retry; Feign timeout is ignored.
Running the test once confirms which configuration is actually applied.
Conclusion
The root cause is that in projects using Ribbon, Feign’s timeout settings are not applied; Ribbon controls the timeout. When configuring timeouts, identify the component that ultimately sends the HTTP request and adjust its parameters accordingly. Multiple layers of abstraction can lead to unexpected behavior if their configurations are not aligned.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer XiaoFu
xiaofucode.com – a programmer learning guide driven by the pursuit of profit
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
