Timeouts and Client Configuration
Set connect and read timeouts for every outbound HTTP call with spring.http.clients properties, per service group, or in code.
spring.http.clients.connect-timeout=1s
spring.http.clients.read-timeout=5s
spring.http.serviceclient.<group>.read-timeout=10s
ClientHttpRequestFactoryBuilder.jdk().build(HttpClientSettings.defaults().withTimeouts(connect, read))A remote call without a timeout can wait for a very long time. If the other service stops answering, every request thread that calls it gets stuck, the pool fills up, and your healthy service goes down with the broken one. Timeouts are not tuning; they are what keeps one slow dependency from taking out everything upstream of it.
Two timeouts
- Connect timeout: how long to wait for the TCP (and TLS) connection. Inside one data centre this should be short, around a second.
- Read timeout: how long to wait for response data once connected. Base it on the remote endpoint's real latency, for example a little above its 99th percentile.
Global settings
Spring Boot applies spring.http.clients.* to the RestClient.Builder, the HTTP service groups and WebClient:
spring.http.clients.connect-timeout=1s
spring.http.clients.read-timeout=5s
spring.http.clients.redirects=dont-followSpring Boot 4 renamed these: the older spring.http.client.* and spring.http.reactiveclient.* keys are no longer bound. They appear only as deprecated metadata pointing at spring.http.clients.*, so setting them changes nothing. Per-group overrides for HTTP interface clients go under spring.http.serviceclient.<group>.*.
Which HTTP library
Boot picks the underlying client from the classpath: Apache HttpClient first, then Jetty, then Reactor Netty, then the JDK's own HttpClient. To choose explicitly, set spring.http.clients.imperative.factory to http-components, jetty, reactor, jdk or simple. The JDK client needs no extra dependency and is a sound default. Apache HttpClient gives you a tunable connection pool.
One client that differs
Build its request factory in code with ClientHttpRequestFactoryBuilder and HttpClientSettings from org.springframework.boot.http.client, then pass it to requestFactory(...) on that client's builder. Inject Boot's own ClientHttpRequestFactoryBuilder<?> and HttpClientSettings beans when you want the same library and defaults with only one value changed.
Budgeting time
Your own callers have a timeout too. If the browser or gateway gives up after 10 seconds, a 30 second read timeout downstream is pointless: you keep working on a request nobody is waiting for. Add up the read timeout times the number of attempts, plus retry delays, and keep it under your own caller's limit. A timed-out call throws ResourceAccessException; map it to 503 or 504.
Example
spring
http
clients
connect-timeout1s
read-timeout5s
redirectsdont-follow
imperative
factoryjdk
serviceclient
catalog
base-urlhttps//catalog.internal.example.com
read-timeout2s
reports
base-urlhttps//reports.internal.example.com
read-timeout30s
When to use it
- Stop a hung reporting service from tying up every request thread by giving all clients a 5 second read timeout.
- Give an autocomplete client a 400 ms read timeout so the search box shows results without suggestions instead of waiting.
- Allow the payment provider 15 seconds while every other dependency keeps the stricter global setting.
More examples
A tighter timeout for one client in code
Boot exposes the <code>ClientHttpRequestFactoryBuilder</code> it selected and the <code>HttpClientSettings</code> built from <code>spring.http.clients.*</code> as beans. Deriving a copy with <code>withTimeouts(connect, read)</code> keeps everything else, such as the SSL bundle and redirect policy, and changes only the timeouts. Search suggestions are a typical case: a late answer is useless, so failing quickly is better.
import java.time.Duration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.http.client.HttpClientSettings;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
record Suggestion(String text, int score) {
}
@Component
public class SearchSuggestClient {
private final RestClient rest;
SearchSuggestClient(RestClient.Builder builder,
ClientHttpRequestFactoryBuilder<?> factoryBuilder,
HttpClientSettings settings,
@Value("${app.suggest.base-url}") String baseUrl) {
// Same HTTP library and defaults Boot chose, but suggestions must be fast or skipped.
ClientHttpRequestFactory factory = factoryBuilder.build(
settings.withTimeouts(Duration.ofMillis(300), Duration.ofMillis(400)));
this.rest = builder.baseUrl(baseUrl).requestFactory(factory).build();
}
public Suggestion[] suggest(String prefix) {
return rest.get()
.uri(uri -> uri.path("/suggest").queryParam("q", prefix).build())
.retrieve()
.requiredBody(Suggestion[].class);
}
}
Choosing the JDK client explicitly
<code>ClientHttpRequestFactoryBuilder.jdk()</code> builds a factory on the JDK's <code>java.net.http.HttpClient</code>, whatever else is on the classpath. Not following redirects is a sensible choice for server-to-server calls: a redirect from an API is usually a configuration mistake, and following it blindly can send credentials to a host you did not intend.
import java.time.Duration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.http.client.HttpClientSettings;
import org.springframework.boot.http.client.HttpRedirects;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
@Configuration(proxyBeanMethods = false)
public class PartnerClientConfig {
@Bean
RestClient partnerRestClient(RestClient.Builder builder, @Value("${app.partner.base-url}") String baseUrl) {
HttpClientSettings settings = HttpClientSettings.defaults()
.withTimeouts(Duration.ofSeconds(2), Duration.ofSeconds(8))
.withRedirects(HttpRedirects.DONT_FOLLOW);
return builder
.baseUrl(baseUrl)
.requestFactory(ClientHttpRequestFactoryBuilder.jdk().build(settings))
.build();
}
}
Global defaults and per-service overrides
Set strict global values and loosen them only where a dependency is known to be slow, so a new client is safe by default. If you name a factory explicitly, add its library as a dependency as well; the property chooses a client, it does not install one. Remember that a 15 second read timeout on payments only makes sense if the endpoint calling it is allowed to take that long.
# Every outbound HTTP client
spring.http.clients.connect-timeout=1s
spring.http.clients.read-timeout=5s
# Prefer Apache HttpClient for its connection pool (needs org.apache.httpcomponents.client5:httpclient5)
spring.http.clients.imperative.factory=http-components
# HTTP interface group "payments": slower provider, own base URL
spring.http.serviceclient.payments.base-url=${PAYMENTS_URL}
spring.http.serviceclient.payments.read-timeout=15s
# RestClient debug output while tuning (development only); connection-level logs come from the HTTP library
logging.level.org.springframework.web.client=DEBUG
Discussion