On Spring Boot 4.2.0-RC2, connect timeout, redirects, cookies, and the proxy belong on java.net.http.HttpClient. The read timeout is applied later, as HttpRequest.timeout. RestClient keeps the URL, headers, and JSON.
I have a RestClient bean that sets a base URL and an auth header. It's built with RestClient.builder() . That is the whole configuration. I had been calling it a client. It isn't one. It's a fluent call in front of whatever HTTP library Spring finds on the classpath, running that library's defaults. The service it lives in also has the webflux starter, so that bean isn't even on the JDK HttpClient . It's on Reactor Netty, with timeouts I never chose. I checked everything below against Spring Boot 4.2.0-RC2 (November 5, 2026, not GA) and the Spring Framework 7.1.0-RC2 it brings. Two objects and an adapter RestClient knows the URL, the headers, the JSON converters, and what a 404 should do. java.net.http.HttpClient knows the connection. Connect timeout, TLS, HTTP version, redirects, cookies, proxy, the executor. It has no base URL. It won't turn a JSON body into a record for you. JdkClientHttpRequestFactory sits between them. It holds one HttpClient and, on every request, sets the read timeout as HttpRequest.timeout . flowchart LR call["GET /roles"] --> rest["RestClient"] rest --> factory["JdkClientHttpRequestFactory"] factory --> jdk["java.net.http.HttpClient"] jdk --> wire["TCP and TLS"] The path and headers are set on RestClient . Connect timeout is set on HttpClient . The read timeout is set on neither. The factory stamps it onto each request. I kept putting all three on RestClient.builder() . Wrong builder. The same GET, written twice This is the call with the JDK client alone. Java 11 or newer. No Spring types. HttpClient httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(2)) .followRedirects(HttpClient.Redirect.NORMAL) .version(HttpClient.Version.HTTP_1_1) .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://partner.example/roles?page=1")) .timeout(Duration.ofSeconds(5)) .header("Accept", "application/json") .header("Authorization", "Bearer " + token) .GET() .build(); HttpResponse<String> response = httpClient.send( request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { throw new IllegalStateException("partner returned " + response.statusCode()); } RolePage page = mapper.readValue(response.body(), RolePage.class); Connect timeout is on the client. The 5 seconds are on the request. Redirects are explicit because the JDK default is Redirect.NEVER . The status check is mine, and so is mapper , the JsonMapper the application already has. I pinned HTTP/1.1 only to show where a JDK-only setting goes. The JDK default is HTTP/2. The same call through RestClient , pinned to the JDK client, on Spring Boot 4.2.0-RC2: @Configuration public class PartnerClientConfig { @Bean RestClient partnerClient( RestClient.Builder builder, HttpClientSettings httpClientSettings, @Value("${partner.token}") String token) { HttpClientSettings settings = httpClientSettings .withTimeouts(Duration.ofSeconds(2), Duration.ofSeconds(5)); ClientHttpRequestFactory requestFactory = ClientHttpRequestFactoryBuilder.jdk() .withHttpClientCustomizer(http -> http.version(HttpClient.Version.HTTP_1_1)) .build(settings); return builder .baseUrl("https://partner.example") .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token) .requestFactory(requestFactory) .build(); } } RolePage page = partnerClient.get() .uri(uri -> uri.path("/roles").queryParam("page", 1).build()) .retrieve() .body(RolePage.class); Two injections do the work. RestClient.Builder is Spring Boot's prototype builder. Boot's JSON converters and any RestClientCustomizer beans are already on it. HttpClientSettings is the bean Spring Boot 4.2 builds from spring.http.clients.* . I start from it and override only the timeouts, so redirects, cookies, and the SSL bundle still come from configuration. HttpClientSettings.defaults() is the other starting point, and it's empty. A factory built from it ignores every spring.http.clients property. The static RestClient.builder() goes further: it ignores Spring Boot entirely and picks its own factory from the classpath. That's the bean I started with. One more 4.2 detail. The 4-argument HttpClientSettings constructor from Spring Boot 4.0 is deprecated since 4.1 and scheduled for removal in 4.3. Use defaults() , the injected bean, and the with* methods. Where each setting actually goes Setting JDK HttpClient RestClient over that client Connect timeout HttpClient.Builder.connectTimeout HttpClientSettings.withConnectTimeout . Boot copies it onto the HttpClient . Read timeout HttpRequest.Builder.timeout HttpClientSettings.withReadTimeout . Boot removes it before building the HttpClient , then calls JdkClientHttpRequestFactory.setReadTimeout . Redirects followRedirects . Default NEVER . HttpRedirects.FOLLOW maps to Redirect.NORMAL . DONT_FOLLOW maps to NEVER . Unset also means NORMAL . Cookies cookieHandler . Default none. HttpCookieHandling.ENABLE installs a CookieManager . defaultCookie and .cookie() only send cookies you name. Proxy proxy . Default ProxySelector.getDefault() . withProxySelector on the JDK builder. withInetAddressFilter wraps that selector. HTTP version version . Default HTTP/2. Not on HttpClientSettings . withHttpClientCustomizer . Base URL, auth header Every request, by hand baseUrl , defaultHeader JSON Your mapper Spring Boot's converters on the injected builder 4xx and 5xx You read statusCode() retrieve() throws a RestClientResponseException subclass The split that matters is the two timeouts. JdkClientHttpRequestFactoryBuilder builds the HttpClient with settings.withReadTimeout(null) , because JdkHttpClientBuilder refuses a read timeout. Then it applies the read timeout to the factory, which puts it on each HttpRequest . So "read timeout" on this stack means the JDK request timeout, and the JDK starts that timer before it opens the connection. A dead host still fails at 5 seconds without a connect timeout. But all 5 seconds go to the connect, and the cause is an HttpTimeoutException , not an HttpConnectTimeoutException . In the logs, "host is down" and "host is slow" look the same. With neither timeout set, the JDK waits. Its javadoc treats a missing request timeout as an infinite one. withTimeouts(connect, read) sets both. I want both. If one policy fits every client, use properties. They feed the HttpClientSettings bean, which the auto-configured factory and the partner bean above both start from. spring: http: clients: connect-timeout: 2s read-timeout: 5s redirects: follow cookie-handling: disable imperative: factory: jdk spring.http.client.connect-timeout , singular, is the Boot 3 key. It has been deprecated since 4.0 in favor of spring.http.clients.connect-timeout , and the 4.2 metadata still marks it that way. Use the plural. Spring Boot won't pick the JDK client just because I asked for RestClient RestClient.builder() and Spring Boot's ClientHttpRequestFactoryBuilder.detect() walk the classpath in the same order: Apache HttpComponents Jetty Reactor Netty java.net.http.HttpClient HttpURLConnection ( SimpleClientHttpRequestFactory ) My service has spring-boot-starter-web and spring-boot-starter-webflux . Webflux brings Reactor Netty, so detection stops at step 3. The JDK client is there. It's never chosen. new JdkClientHttpRequestFactory() is the other trap. The no-arg constructor calls HttpClient.newHttpClient() . That client prefers HTTP/2, follows no redirects, and has no connect timeout. Spring doesn't change any of that. Pin the factory: spring.http.clients.imperative.factory=jdk or call ClientHttpRequestFactoryBuilder.jdk() in code, the way the partner bean does. The legal property values on 4.2.0-RC2 are http-components , jetty , reactor , jdk , and simple . For a corporate proxy, I add a customizer instead of declaring my own ClientHtt...