TL;DR
Your HTTP client connected to the server โ but the server never sent data back in time. By default, HttpURLConnection sets read timeout to 0, meaning it waits forever. Set explicit timeouts: 5โ10 seconds for connect, 10โ30 seconds for read. Add retry logic for flaky networks.
What the error looks like
java.net.SocketTimeoutException: Read timed out
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at sun.security.ssl.InputRecord.readFully(InputRecord.java:465)
...
The TCP connection succeeded โ so DNS and firewall are fine. But no response arrived within the timeout window. Typical culprits: slow backend queries, large payloads (think 50 MB CSV exports), network congestion, or the server silently dropping the connection mid-transfer.
Fix by HTTP client
HttpURLConnection (standard library)
No read timeout is set by default โ it blocks forever. Always configure both explicitly:
URL url = new URL("https://api.example.com/data");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5_000); // 5s to establish TCP connection
conn.setReadTimeout(15_000); // 15s to receive data after connected
conn.setRequestMethod("GET");
try (InputStream in = conn.getInputStream()) {
// read response
}
Apache HttpClient 5.x
RequestConfig config = RequestConfig.custom()
.setConnectionRequestTimeout(Timeout.ofSeconds(5))
.setConnectTimeout(Timeout.ofSeconds(5))
.setResponseTimeout(Timeout.ofSeconds(15)) // this is the read timeout
.build();
try (CloseableHttpClient client = HttpClients.custom()
.setDefaultRequestConfig(config)
.build()) {
HttpGet request = new HttpGet("https://api.example.com/data");
try (CloseableHttpResponse response = client.execute(request)) {
// handle response
}
}
On Apache HttpClient 4.x, swap setResponseTimeout() for setSocketTimeout() โ the parameter name changed between major versions.
OkHttp
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.build();
Request request = new Request.Builder()
.url("https://api.example.com/data")
.build();
try (Response response = client.newCall(request).execute()) {
// handle response
}
Spring RestTemplate
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(5_000);
factory.setReadTimeout(15_000);
RestTemplate restTemplate = new RestTemplate(factory);
String result = restTemplate.getForObject("https://api.example.com/data", String.class);
Prefer the Apache HttpClient backend for production? Wire it up as a bean:
@Bean
public RestTemplate restTemplate() {
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(5_000);
factory.setConnectionRequestTimeout(5_000);
factory.setReadTimeout(15_000);
return new RestTemplate(factory);
}
Spring WebClient (reactive)
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5_000)
.responseTimeout(Duration.ofSeconds(15));
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
Add retry logic for transient failures
One timeout doesn't mean the server is dead. Networks are flaky. Wrap the call with retry logic so brief hiccups don't surface as errors to your users:
// Using Spring Retry
@Retryable(
value = { SocketTimeoutException.class, ResourceAccessException.class },
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2)
)
public String callApi() {
return restTemplate.getForObject(url, String.class);
}
Rather not pull in Spring Retry? Roll it manually with exponential backoff:
int maxRetries = 3;
int delayMs = 1000;
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
return callExternalApi();
} catch (SocketTimeoutException e) {
if (attempt == maxRetries) throw new RuntimeException("All retries failed", e);
Thread.sleep(delayMs * attempt);
}
}
Choosing the right timeout values
- **Connect timeout**: 3โ10 seconds. A healthy TCP handshake completes in milliseconds on a local network, under a second across the internet.
- **Read timeout**: Depends on the endpoint. Fast REST APIs: 10โ15s. Slow data exports or file downloads: 60โ300s.
- **Never set read timeout to 0** in production โ a hung server will hold your thread hostage indefinitely.
Quick rule of thumb: set read timeout to twice the expected worst-case response time for that specific endpoint. If a report endpoint normally responds in 8 seconds, set 16โ20 seconds.
Debugging the root cause
Bumping the timeout is a band-aid. First, find out why the server is slow:
- Check server-side logs around the exact time of the timeout
- Reproduce outside Java: `curl --max-time 15 https://api.example.com/data`
- Sporadic timeouts usually point to network issues. Consistent ones point to server performance.
- Add timestamps to your logs to pinpoint which step is slow
Behind a corporate proxy or VPN? Routing problems can cause silent read stalls that look identical to server slowness. The Subnet Calculator at toolcraft.app can help verify CIDR ranges and routing paths when debugging reachability between services.
Verify the fix
- Simulate the timeout: point at a slow endpoint, or add `Thread.sleep(20_000)` in a test server handler
- Confirm the exception fires after your configured timeout โ exactly 15 seconds, not 30
- Run a normal request and confirm it completes well within the threshold
- Check logs: `SocketTimeoutException` should appear at a predictable time, not after a random delay
Connection pool exhaustion (related issue)
Reuse your HTTP client โ never create a new CloseableHttpClient or OkHttpClient per request. When timeouts pile up without being handled, they exhaust the connection pool and trigger cascading failures across your service. Set pool limits explicitly:
// Apache HttpClient 5.x
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(100);
cm.setDefaultMaxPerRoute(20);
CloseableHttpClient client = HttpClients.custom()
.setConnectionManager(cm)
.setDefaultRequestConfig(config)
.evictExpiredConnections()
.build();

