Think of an external API like a phone call: if nobody answers, you need to hang up on purpose instead of waiting forever.
Question: How do you handle timeout problems when your Spring Boot app calls an external API?
Answer: I set a clear time limit for the outbound call, so a slow dependency cannot block my app forever. In Spring Boot, that usually means configuring connect timeout, response/read timeout, and an overall request timeout, then mapping timeout failures to a safe fallback or a 504-style error. The key idea is to fail fast, protect resources, and only retry when the call is safe to repeat.
Interview-Ready Answer: I handle external API timeouts by setting three guardrails: a short connect timeout, a response or read timeout, and an overall request timeout. In Spring Boot, I usually do this with WebClient or RestClient, then I convert timeout failures into either a safe fallback or a clean error response, depending on the business case. I also avoid blind retries unless the request is idempotent, because retries can create duplicate side effects and make an outage worse.
Timeout handling is the rule that says, “I will wait this long, and no longer.” That sounds simple, but it protects three things at once: user experience, server resources, and downstream dependency health. In practice, it stops one slow partner API from tying up your whole Spring Boot service.
Mono.timeout(...) is a common way to enforce that full budget.TimeoutException or a wrapped network timeout. In WebFlux, cancellation is important because it stops the client-side pipeline and frees the connection sooner.| Timeout | Protects | Typical use |
|---|---|---|
| Connect | Dead host | Fail fast on network setup |
| Read / response | Slow server | Wait only for data gaps |
| Overall | Your SLA | Cap the whole request time |
| Retry | Transient blips | Only for safe, repeatable calls |
Use timeouts anywhere your app depends on another service: payments, tax, shipping, fraud checks, chat, search, and login. The reason is simple: a timeout is a circuit breaker for time, and time is the most expensive resource in a busy server. If 100 requests each wait 5 seconds, you have burned 500 thread-seconds or connection-seconds for almost no useful work.
In modern Spring Boot, WebClient is the best fit for high-concurrency, non-blocking services. If your app is classic Spring MVC and blocking is fine, RestClient works too; the concept stays the same, but the client configuration point changes.
Memory angle: think of timeouts as a three-stage safety rope: first you check whether the door opens, then you wait for a reply, then you stop the whole conversation when your patience budget runs out.
Imagine a checkout service in an e-commerce app. It calls a tax API before finalizing an order, because the final price depends on region and tax rules. One day the tax provider starts taking 8 to 12 seconds during peak load.
Before timeout handling, the checkout service keeps waiting. In a blocking MVC app, worker threads pile up, the request queue grows, and users see spinning loaders that end in 504s. In logs, you might see messages like java.net.SocketTimeoutException: Read timed out only after someone finally adds a timeout, or WebClientRequestException when the client cannot get a response in time. If the team adds naive retries without idempotency, a single checkout can even be submitted twice.
The business symptom is ugly: cart abandonment rises, payment attempts look flaky, and support gets tickets saying “the site hangs on place order.” The fix is usually to cap the call at a sensible SLA, show a graceful fallback like “tax temporarily unavailable, try again,” and emit metrics so the team can see the dependency slow down before the outage becomes visible to users.
package com.example.timeoutdemo;
import io.netty.channel.ChannelOption;
import java.time.Duration;
import java.util.concurrent.TimeoutException;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;
@SpringBootApplication
public class ExternalApiTimeoutApplication {
public static void main(String[] args) {
SpringApplication.run(ExternalApiTimeoutApplication.class, args);
}
@Bean
WebClient webClient(WebClient.Builder builder) {
// A short connect timeout fails fast when the host is unreachable.
// responseTimeout protects the wait for the server to actually send data.
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000)
.responseTimeout(Duration.ofSeconds(2));
return builder
.baseUrl("http://localhost:8080")
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
@Bean
ExternalApiService externalApiService(WebClient webClient) {
return new ExternalApiService(webClient);
}
@Bean
CommandLineRunner runner() {
return args -> {
System.out.println("Try these URLs after startup:");
System.out.println(" curl 'http://localhost:8080/api/proxy?delayMs=500'");
System.out.println(" curl 'http://localhost:8080/api/proxy?delayMs=2500'");
};
}
@RestController
static class SlowUpstreamController {
@GetMapping("/upstream/slow")
Mono<String> slow(@RequestParam(defaultValue = "0") long delayMs) {
long safeDelay = Math.max(0, delayMs);
// Mono.delay is non-blocking, which is important in WebFlux.
return Mono.delay(Duration.ofMillis(safeDelay))
.map(ignored -> "upstream finished after " + safeDelay + " ms");
}
}
@RestController
static class ProxyController {
private final ExternalApiService service;
ProxyController(ExternalApiService service) {
this.service = service;
}
@GetMapping("/api/proxy")
Mono<String> proxy(@RequestParam(defaultValue = "0") long delayMs) {
return service.fetchSlowResource(delayMs);
}
}
static class ExternalApiService {
private final WebClient webClient;
ExternalApiService(WebClient webClient) {
this.webClient = webClient;
}
Mono<String> fetchSlowResource(long delayMs) {
long safeDelay = Math.max(0, delayMs);
return webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/upstream/slow")
.queryParam("delayMs", safeDelay)
.build())
.retrieve()
.bodyToMono(String.class)
// This is the overall guardrail for the entire request.
.timeout(Duration.ofSeconds(3))
// If the upstream is too slow, return a safe fallback instead of failing hard.
.onErrorResume(this::isTimeoutLike, ex ->
Mono.just("fallback: external API timed out, returning safe default"));
}
private boolean isTimeoutLike(Throwable ex) {
Throwable current = ex;
while (current != null) {
if (current instanceof TimeoutException) {
return true;
}
String name = current.getClass().getName();
if (name.contains("ReadTimeoutException") || name.contains("ConnectTimeoutException")) {
return true;
}
current = current.getCause();
}
return false;
}
}
}
Follow-up & Tricky Questions:
timeout() in the reactive chain. In RestClient or RestTemplate, you configure the underlying HTTP request factory with connect and read limits.timeout() stop the server from working? No. It cancels the client-side wait, but the upstream service may still continue processing. That is why side effects must be designed carefully, especially for write requests.Common Mistakes:
Mono and WebClient; a blocking sleep or block() can hurt concurrency.Memory Hook: Knock, talk, hang up. Knock = connect timeout, talk = read timeout, hang up = overall timeout.
Cheat Sheet:
Practice Tasks: