DEV Community

AK DevCraft
AK DevCraft Subscriber

Posted on

Never Use Spring RestClient Default Implementation in Production

🚨 Problem Introduction

Imagine you’re building a Spring Boot Order Service that calls three downstream services:

  • Inventory Service
  • Pricing Service
  • Shipping Service

You decide to use Spring’s new RestClient (Spring 6 / Boot 3). Locally, everything works perfectly fine. But in production, under load, issues appear:

  • Requests pile up
  • Threads block waiting for connections
  • Users face delays and even timeouts

What’s going on?

🔎 As most likely you know based on my previous blog Avoid Spring RestTemplate Default Implementation to Prevent Performance Impact. The
RestTemplate defaults only 2 total global connections (without Apache HttpClient tuning).

Welp! RestClient defaults does slightly better with 5 per-host connections.

👉 But that’s still tiny for production load.
If 100 users call your service simultaneously:

  • Only 5 calls to downstream services can proceed
  • 95 calls sit waiting in the connection queue
  • Latency spikes
  • Errors creep in

This is why things work locally but fail under load — you’re bottlenecked by the connection pool defaults.

🧪 Conceptual Benchmark

Scenario: 100 concurrent requests from OrderService → InventoryService

With Default RestClient (5 connections/host):

  • Only 5 requests proceed at a time.
  • 95 requests wait
  • Latency grows
  • Timeouts are likely under heavy load

Tuned RestClient (150 total, 50 per-host):

  • Pool size reflects expected traffic — e.g., ~150 active users per second.
  • Up to 50 concurrent requests per external service can proceed in parallel.
  • Remaining requests queue briefly, but overall throughput remains smooth.
  • Response times stay predictable and stable.

The point isn’t the exact number (150 or 50) — it’s about matching pool capacity to your app’s concurrency profile.
Your pool size should be proportional to peak concurrent users and downstream service latency.

✅ The Solution

Add Dependency

Apache HttpClient 5 dependency in the project, maven example:

<dependencies>
 <!-- Apache HttpClient 5 (for advanced pooling, timeouts, etc.) -->
    <dependency>
        <groupId>org.apache.httpcomponents.client5</groupId>
        <artifactId>httpclient5</artifactId>
    </dependency>
</dependencies>
Enter fullscreen mode Exit fullscreen mode

Code Update

Configure your own HTTP connection pool

import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestClient;

public class RestClientConfig {

    public RestClient restClient() {
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();

        connectionManager.setMaxTotal(150);          // total connections
        connectionManager.setDefaultMaxPerRoute(50); // per-host connections

        CloseableHttpClient httpClient = HttpClients.custom()
                .setConnectionManager(connectionManager)
                .build();

        return RestClient.builder()
                .requestFactory(new HttpComponentsClientHttpRequestFactory(httpClient))
                .build();
    }
}
Enter fullscreen mode Exit fullscreen mode

⚙️ Bonus: Don’t Forget the Timeouts

Even with connection pooling tuned, your app can still hang if remote services become slow.
That’s where timeouts come in — they protect threads from waiting indefinitely.

import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestClient;

public class RestClientConfig {

    public RestClient restClient() {
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
        connectionManager.setMaxTotal(150);
        connectionManager.setDefaultMaxPerRoute(50);

        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(2000)            // time to establish connection (ms)
                .setResponseTimeout(3000)           // time waiting for server response (ms)
                .setConnectionRequestTimeout(1000)  // time to wait for connection from pool (ms)
                .build();

        CloseableHttpClient httpClient = HttpClients.custom()
                .setConnectionManager(connectionManager)
                .setDefaultRequestConfig(requestConfig)
                .build();

        return RestClient.builder()
                .requestFactory(new HttpComponentsClientHttpRequestFactory(httpClient))
                .build();
    }
}
Enter fullscreen mode Exit fullscreen mode

💡 Quick Notes

  • Timeouts above are in milliseconds (2000 ms = 2 seconds)
  • connectTimeout: protects you from unresponsive hosts.
  • connectionRequestTimeout: stops threads from waiting too long for a pooled connection.
  • responseTimeout: prevents hanging on slow downstream responses.

Combined with a tuned connection pool, these timeouts make your RestClient fast, safe, and production-hardened.

Conclusion - Key Takeaways

  • RestTemplate default = 2 global connections.
  • RestClient default = 5 per-host connections.
  • Configured pool = production-safe scaling.

⚠️ Never trust defaults for HTTP clients. They’re tuned for safety in local/dev, not performance in production.

TL;DR

  • RestClient (Spring Boot 3 / Spring 6) is better than RestTemplate, but still not production-ready out of the box.
  • Default pool size = 5 per host → easily becomes a bottleneck under load.
  • Always configure:
    • ✅ Connection pool size (maxTotal, maxPerRoute)
    • ✅ Timeouts (connect, request, read)
  • Keep your RestClient tuned, and it’ll handle thousands of concurrent requests gracefully.

💡 In short:
Spring’s RestClient is powerful — but like any engine, it needs tuning before you take it to production.

If you have reached here, then I have made a satisfactory effort to keep you reading. Please be kind enough to leave any comments or share with corrections.

My Other Blogs:

Top comments (0)