A response timeout happens when a client (browser, mobile app, another microservice, or a load balancer) gives up waiting for your Spring Boot API to finish responding. Unlike a request timeout (client failing to send data in time), a response timeout means the server accepted the request but couldn’t finish processing and writing the response before the caller’s patience ran out.
Table of Contents
- Response Timeout vs Request Timeout vs Read Timeout
- Where the Timeout Actually Happens
- Root Causes
- Fix 1: Set Realistic Timeouts at Every Layer
- Fix 2: Eliminate Blocking Calls on Request Threads
- Fix 3: Size Thread Pools and Connection Pools Correctly
- Fix 4: Add Circuit Breakers for Downstream Calls
- Common Mistakes
- Best Practices Checklist
- Troubleshooting Decision Tree
- Interview Questions
- FAQs
Response Timeout vs Request Timeout vs Read Timeout
- Request timeout: the client is too slow sending the request body (see How to Fix Request Timeout in Spring Boot REST APIs).
- Response timeout: the server accepted the request but takes too long to produce and send back a response.
- Read timeout: usually refers to your Spring Boot app acting as a client, waiting too long for a downstream service’s response.
Response timeouts are almost always a symptom of something slow happening between “request received” and “response written” — a slow query, a slow downstream call, or a starved thread pool.
Where the Timeout Actually Happens
Client ──▶ Load Balancer ──▶ API Gateway ──▶ Spring Boot ──▶ DB / Downstream API
│ │ │ │ │
│◀── each layer has its own timeout clock ───────┴──────────────────┘
Any layer can be the one that gives up first. A 504 Gateway Timeout from NGINX or an ALB often means Spring Boot was going to respond eventually — just not before the gateway’s own timeout fired.
Root Causes
- Slow database queries — missing indexes, N+1 queries, or large unpaginated result sets.
- Slow synchronous downstream calls — a REST call to another microservice with no timeout configured, so the calling thread blocks indefinitely.
- Thread pool exhaustion — Tomcat’s worker threads are all occupied by other slow requests.
- GC pauses — a long stop-the-world pause can delay response writing enough to trip a tight client timeout.
- Chained microservice latency — each hop adds latency; five 800ms hops can exceed the caller’s timeout even though no single hop looks slow.
Fix 1: Set Realistic Timeouts at Every Layer
@Bean
public RestClient restClient() {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.DEFAULTS
.withConnectTimeout(Duration.ofSeconds(2))
.withReadTimeout(Duration.ofSeconds(5));
return RestClient.builder()
.requestFactory(ClientHttpRequestFactories.get(settings))
.build();
}
server:
tomcat:
connection-timeout: 20000
spring:
mvc:
async:
request-timeout: 15000
A timeout that’s too long doesn’t protect anything; a timeout that’s too short causes false failures under normal load. Base the value on measured p99 latency, not a guess.
Fix 2: Eliminate Blocking Calls on Request Threads
@GetMapping("/dashboard")
public DashboardResponse getDashboard() {
var orders = orderClient.getOrders(); // blocks thread
var invoices = invoiceClient.getInvoices(); // blocks thread again, sequentially
return new DashboardResponse(orders, invoices);
}
Run independent downstream calls concurrently instead of sequentially:
@GetMapping("/dashboard")
public DashboardResponse getDashboard() {
CompletableFuture<List<Order>> ordersFuture =
CompletableFuture.supplyAsync(orderClient::getOrders, executor);
CompletableFuture<List<Invoice>> invoicesFuture =
CompletableFuture.supplyAsync(invoiceClient::getInvoices, executor);
CompletableFuture.allOf(ordersFuture, invoicesFuture).join();
return new DashboardResponse(ordersFuture.join(), invoicesFuture.join());
}
This alone can cut total response time roughly in half when two independent calls previously ran back-to-back.
Fix 3: Size Thread Pools and Connection Pools Correctly
server:
tomcat:
threads:
max: 200
min-spare: 20
spring:
datasource:
hikari:
maximum-pool-size: 20
connection-timeout: 3000
If the Tomcat thread pool is smaller than the sustained request rate × average response time, requests queue and appear to time out even though each individual request is fast in isolation. This ties directly into the causes discussed in Spring Boot REST API Slow Response (Performance Tuning Guide).
Fix 4: Add Circuit Breakers for Downstream Calls
@CircuitBreaker(name = "invoiceService", fallbackMethod = "invoiceFallback")
@TimeLimiter(name = "invoiceService")
public CompletableFuture<List<Invoice>> getInvoices() {
return CompletableFuture.supplyAsync(invoiceClient::getInvoices);
}
private CompletableFuture<List<Invoice>> invoiceFallback(Throwable t) {
return CompletableFuture.completedFuture(Collections.emptyList());
}
A circuit breaker (Resilience4j) stops a single slow downstream dependency from turning into a full response timeout for every caller, by failing fast once the dependency is known to be unhealthy.
Common Mistakes
- Leaving
RestTemplate/WebClientwith no explicit connect/read timeout. - Making independent downstream calls sequentially instead of concurrently.
- Increasing the gateway timeout to “fix” a timeout instead of fixing the slow operation underneath.
- Not distinguishing a genuinely slow query from thread pool exhaustion.
- Ignoring GC logs when timeouts are intermittent rather than constant.
Best Practices Checklist
- [ ] Explicit connect/read timeouts on every outbound HTTP client
- [ ] Independent downstream calls parallelized, not chained
- [ ] Tomcat thread pool and HikariCP pool sized against measured load
- [ ] Circuit breakers on non-critical downstream dependencies
- [ ] Gateway/load-balancer timeout set slightly higher than the app’s own timeout
- [ ] p99 latency monitored per endpoint, not just averages
Troubleshooting Decision Tree
Response timeout reported
│
├─ Happens only under load? → Check thread pool / connection pool saturation
├─ Happens consistently on one endpoint? → Profile that endpoint's DB queries
├─ Happens after a deploy? → Check for new synchronous downstream calls
├─ Intermittent, no clear pattern? → Check GC logs for long pauses
└─ 504 from gateway but app logs show success? → Align gateway timeout with app timeout
Interview Questions
- What’s the difference between a connect timeout and a read timeout?
- How does thread pool exhaustion cause response timeouts even when the database is healthy?
- Why can parallelizing downstream calls reduce response timeouts more effectively than increasing timeout values?
- What role does a circuit breaker play in preventing cascading timeouts?
- How would you tell apart a slow query problem from a saturated connection pool using metrics alone?
FAQs
Q: Should I just increase the timeout value whenever I see a response timeout?
Only as a short-term mitigation. Increasing the timeout without addressing the underlying slow operation just delays the failure and can make thread pool exhaustion worse.
Q: Why do I get a 504 from my load balancer but my Spring Boot logs show the request completed successfully?
The load balancer’s own timeout fired before the app finished, so it returned 504 to the client while the app kept processing in the background.
