Timeout Pattern in Microservices
Building resilient microservices: a deep dive into the Timeout pattern. In a microservices system, services constantly call each other over the network. Sooner or later, one of those calls will hang: a service freezes, a database locks up, or a network link silently drops packets. The Timeout pattern makes sure your service never waits forever.
This guide explains why timeouts are the most important resilience setting, the different types of timeouts, and how to configure them in Java and Spring Boot: HTTP clients, CompletableFuture, Resilience4j, databases and gateways. It also covers how to choose values, timeout budgets across several services, and what to do after a timeout.
1. What Is the Timeout Pattern?
The Timeout pattern sets a maximum time that a service will wait for an operation, such as a call to another service or a database query. If the operation does not finish in time, the caller stops waiting, frees its resources and handles the situation, for example by returning a fallback or an error.
Think of ordering food at a restaurant. If your meal has not arrived after 30 minutes, you do not sit there all night; you ask the staff, order something else, or leave. A timeout is that limit, decided in advance.
2. Why Waiting Forever Is Dangerous
A slow service is often more dangerous than a service that is completely down. A down service fails fast: the connection is refused immediately. A slow or hung service keeps the caller waiting, holding a thread, a connection and memory the whole time.
Payment Service hangs
|
Order Service threads wait for Payment -> all 200 threads busy
|
API Gateway waits for Order Service -> gateway slows down
|
Users see the whole shop as "down" -> even pages that never use payments
This is a cascading failure: one slow dependency exhausts the resources of every service that calls it, directly or indirectly. Many HTTP clients and drivers have very long or even infinite default timeouts, so without explicit configuration this can happen in any system.
3. Types of Timeouts
| Timeout | What it limits | Typical value inside a data center |
|---|---|---|
| Connect timeout | Time to open a network connection to the server | 0.5–2 s |
| Read (response) timeout | Time to wait for response data once connected | 1–5 s, based on the service |
| Connection pool timeout | Time to wait for a free connection from a pool | 0.5–3 s |
| Overall (total) timeout | Total time for the whole operation, including retries | Within the user’s time budget |
| Idle timeout | How long an unused connection stays open | Tens of seconds to minutes |
| Query / transaction timeout | Maximum time for a database query or transaction | 1–10 s for user requests |
A connect timeout alone is not enough: a server can accept the connection quickly and then never send a response. Always set both a connect and a read timeout, and for user-facing requests also an overall limit.
4. How a Timeout Works
0.0 s Order Service sends request to Payment Service
... waiting ...
2.0 s No response yet -> timeout fires
Order Service stops waiting, frees the thread
-> returns a fallback / error / tries again (if safe)
(Payment Service may still be working on the request!)
The last line matters: a timeout only stops the caller from waiting. The other service may still finish the work later. Section 13 explains how to handle this safely.
5. A Practical Example: Order Service and a Slow Payment Service
An order service calls a payment service, which normally answers in about 200 ms.
5.1 Without a timeout
- The payment service hits a database lock and stops responding.
- Every order request now waits, possibly for minutes.
- Within seconds, all of the order service’s request threads are waiting.
- The order service stops answering any request, including “view my orders”, which does not need payments at all.
5.2 With a timeout
- The order service waits at most 2 seconds for the payment service.
- After 2 seconds, it stops waiting and tells the user: “We could not confirm your payment yet. Please try again in a moment.”
- Threads are freed quickly, so other features keep working.
- Combined with a circuit breaker, the order service soon stops calling the stuck payment service altogether, until it recovers.
6. Timeouts on HTTP Clients in Spring
6.1 RestClient
Configure timeouts once, for every RestClient.Builder Spring injects, using a customizer. This example uses the JDK’s built-in HTTP client:
package com.example.order.config;
import java.net.http.HttpClient;
import java.time.Duration;
import org.springframework.boot.web.client.RestClientCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.JdkClientHttpRequestFactory;
@Configuration
public class HttpTimeoutConfig {
@Bean
public RestClientCustomizer httpTimeouts() {
HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(1)) // connect timeout
.build();
JdkClientHttpRequestFactory factory = new JdkClientHttpRequestFactory(httpClient);
factory.setReadTimeout(Duration.ofSeconds(2)); // response timeout
return builder -> builder.requestFactory(factory);
}
}
If one dependency needs different values, for example a slow reporting service, build its client with its own request factory instead of the shared one.
6.2 WebClient
For reactive code, set timeouts on the underlying Reactor Netty client:
package com.example.order.config;
import java.time.Duration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import io.netty.channel.ChannelOption;
import reactor.netty.http.client.HttpClient;
@Configuration
public class WebClientConfig {
@Bean
public WebClient inventoryWebClient(WebClient.Builder builder) {
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1_000) // connect timeout
.responseTimeout(Duration.ofSeconds(2)); // response timeout
return builder
.baseUrl("http://inventory-service")
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
}
You can also limit a single call with the Reactor timeout operator, for example .bodyToMono(Stock.class).timeout(Duration.ofSeconds(2)).
6.3 OpenFeign
spring:
cloud:
openfeign:
client:
config:
default: # all Feign clients
connect-timeout: 1000 # milliseconds
read-timeout: 2000
payment-service: # override for one client
read-timeout: 4000
7. Timeouts for Any Code with CompletableFuture
Some operations have no built-in timeout setting, such as a call to a third-party library. Java’s CompletableFuture can put a time limit on any work:
// Fail with a TimeoutException if the work takes longer than 2 seconds
CompletableFuture<Quote> quote = CompletableFuture
.supplyAsync(() -> shippingProvider.getQuote(order), executor)
.orTimeout(2, TimeUnit.SECONDS);
// Or complete with a default value instead of failing
CompletableFuture<List<Product>> recommendations = CompletableFuture
.supplyAsync(() -> recommendationClient.forCustomer(customerId), executor)
.completeOnTimeout(List.of(), 500, TimeUnit.MILLISECONDS);
| Method | On timeout | Use for |
|---|---|---|
orTimeout(time, unit) | Completes the future with a TimeoutException | Required data, where the caller must react to the failure |
completeOnTimeout(value, time, unit) | Completes the future with the given default value | Optional data, such as recommendations or reviews |
8. Timeouts with Resilience4j TimeLimiter
Resilience4j’s TimeLimiter adds timeouts declaratively and works well with its retry and circuit breaker modules:
resilience4j:
timelimiter:
instances:
shipping:
timeout-duration: 2s
cancel-running-future: true # try to cancel the work when time is up
package com.example.order.shipping;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeoutException;
import org.springframework.stereotype.Service;
import io.github.resilience4j.timelimiter.annotation.TimeLimiter;
@Service
public class ShippingQuoteService {
private final ShippingProviderClient provider;
public ShippingQuoteService(ShippingProviderClient provider) {
this.provider = provider;
}
@TimeLimiter(name = "shipping", fallbackMethod = "standardQuote")
public CompletableFuture<ShippingQuote> quote(Order order) {
return CompletableFuture.supplyAsync(() -> provider.getQuote(order));
}
// Used when the provider does not answer within 2 seconds
private CompletableFuture<ShippingQuote> standardQuote(Order order, TimeoutException ex) {
return CompletableFuture.completedFuture(ShippingQuote.standardRate(order));
}
}
The @TimeLimiter annotation works on methods returning CompletableFuture (or reactive types). It requires the resilience4j-spring-boot3 dependency and Spring AOP.
9. Database and Transaction Timeouts
Databases are a common source of hanging requests: a missing index, a lock held by another transaction, or a pool with no free connections. Set limits at each level:
# Wait at most 3 s for a free connection from the HikariCP pool (milliseconds)
spring.datasource.hikari.connection-timeout=3000
# Default maximum time for JPA queries (milliseconds)
spring.jpa.properties.jakarta.persistence.query.timeout=5000
@Service
public class ReportService {
private final OrderRepository orders;
public ReportService(OrderRepository orders) {
this.orders = orders;
}
// The whole transaction may take at most 10 seconds
@Transactional(readOnly = true, timeout = 10)
public SalesReport monthlyReport(YearMonth month) {
return SalesReport.from(orders.findAllByMonth(month));
}
}
Long-running reports should not share a tight budget with customer requests: give them their own timeouts, and ideally their own connection pool or a read replica, so they cannot slow down checkout.
10. Server-Side and Gateway Timeouts
Timeouts belong on the receiving side too:
| Layer | Setting | Purpose |
|---|---|---|
| API gateway (Spring Cloud Gateway) | spring.cloud.gateway.server.webflux.httpclient.response-timeout | Stop waiting for slow backend services |
| Embedded Tomcat | server.tomcat.connection-timeout | Close connections from clients that send data too slowly |
| Spring MVC async requests | spring.mvc.async.request-timeout | Limit asynchronous request processing |
| Service mesh (Istio) | timeout in a VirtualService | Enforce a timeout on every call, without code changes |
| Load balancer | Idle / request timeout settings | Close stuck connections at the edge |
spring:
cloud:
gateway:
server:
webflux:
httpclient:
connect-timeout: 1000 # milliseconds
response-timeout: 5s # longest any route may take
The gateway’s timeout should be the longest in the chain, because it covers everything behind it. See the API Gateway pattern.
11. Choosing Good Timeout Values
A timeout that is too short fails healthy requests; one that is too long lets problems spread. Base values on measurements:
- Measure the dependency’s response times, especially the 99th percentile (p99): the time within which 99% of requests finish.
- Set the read timeout a little above the p99, for example p99 × 1.5, so normal slow requests still succeed.
- Check that the value fits inside the caller’s own time budget (section 12).
- Review the values when traffic or code changes.
| Dependency | Measured p99 | Suggested read timeout |
|---|---|---|
| Inventory lookup | 120 ms | 200–300 ms |
| Payment provider | 1.2 s | 2 s |
| Product search | 400 ms | 600–800 ms |
| Monthly report (background) | 40 s | 60 s, in a separate path from user requests |
Spring Boot Actuator with Micrometer records HTTP client and server response times, including percentiles, so you can read these numbers from your own monitoring.
12. Timeout Budgets and Deadline Propagation
When one user request passes through several services, each layer’s timeout must fit inside the layer above it. Otherwise an inner service keeps working on a request that the outer layer has already given up on:
User request budget ................................ 3.0 s
API Gateway response timeout ..................... 3.0 s
Order Service total work ....................... 2.5 s
Inventory call (read timeout) ................ 0.4 s
Payment call (read timeout) .................. 1.5 s
Database queries (query timeout) ............. 0.5 s
Rule: every inner timeout is shorter than the outer one.
Better still, pass the remaining time along, so each step knows how much time is left. gRPC does this automatically with deadlines. For your own code, a small helper can track the budget:
package com.example.common.timeout;
import java.time.Duration;
import java.time.Instant;
public final class Deadline {
private final Instant expiresAt;
private Deadline(Instant expiresAt) {
this.expiresAt = expiresAt;
}
public static Deadline after(Duration budget) {
return new Deadline(Instant.now().plus(budget));
}
// Time left; never negative
public Duration remaining() {
Duration left = Duration.between(Instant.now(), expiresAt);
return left.isNegative() ? Duration.ZERO : left;
}
public boolean isExpired() {
return remaining().isZero();
}
}
public OrderResult placeOrder(PlaceOrderRequest request) {
Deadline deadline = Deadline.after(Duration.ofMillis(2500));
Reservation reservation = inventoryClient.reserve(request);
if (deadline.isExpired()) {
inventoryClient.release(reservation.id());
throw new RequestTimedOutException("Order could not be completed in time");
}
// Give the payment call only the time that is really left
long remainingMs = deadline.remaining().toMillis();
PaymentResult payment = CompletableFuture
.supplyAsync(() -> paymentClient.charge(request), executor)
.orTimeout(remainingMs, TimeUnit.MILLISECONDS)
.join();
return OrderResult.confirmed(reservation, payment);
}
Between services, the remaining time can be sent in a request header, so the next service can stop early if the caller has already given up.
13. What to Do After a Timeout
A timeout tells you that you did not get an answer in time, not that the operation failed. The other service may still complete it. Decide in advance how to react:
| Option | When to use it | Watch out for |
|---|---|---|
| Return a fallback | Optional data: recommendations, reviews, estimates | Make it clear to users when data is incomplete |
| Retry | Idempotent operations, such as reading data | Only within the remaining time budget; use backoff |
| Report “pending” | Important actions that may have succeeded, such as payments | Check the real status later, or let the other service notify you |
| Return an error | When nothing useful can be shown | Use 504 Gateway Timeout or 503 with a clear message |
For actions that change data, always send an idempotency key, such as the order ID. Then a retry after a timeout can never charge a customer twice, even if the first request actually succeeded. See the Retry pattern.
14. Combining Timeouts with Other Resilience Patterns
| Pattern | How it works with timeouts |
|---|---|
| Retry | A timed-out call can be retried, if safe; each attempt has its own timeout, and all attempts fit in the total budget |
| Circuit breaker | Counts timeouts and slow calls as failures; when too many occur, it stops calling the service for a while |
| Bulkhead | Limits how many calls can wait at the same time, so even short timeouts cannot use up every thread |
| Fallback | Gives users a useful answer when the timeout fires |
Timeouts are the foundation: without them, retries never get a chance to run, and circuit breakers only notice failures far too late.
15. Monitoring Timeouts
- Count timeouts per dependency; a sudden rise usually means a service is slowing down.
- Track response-time percentiles (p50, p95, p99) and compare them with your timeout values.
- Log timeouts with the trace ID, so you can see in the trace which call was slow.
- Alert when timeouts exceed a small percentage of calls.
- Watch for values that are too tight: if healthy calls often time out, raise the limit or fix the slow path.
16. Best Practices
- Set a timeout on every remote call: HTTP, gRPC, database, cache and message broker clients.
- Never rely on defaults; many are very long or infinite.
- Set both connect and read timeouts, plus an overall limit for user requests.
- Base values on measured percentiles, not guesses.
- Make inner timeouts shorter than outer ones, and keep everything within the user’s time budget.
- Configure timeouts centrally, with per-dependency overrides where needed.
- Plan the reaction: fallback, safe retry, “pending” status or a clear error.
- Use idempotency keys for operations that change data.
- Combine with circuit breakers and bulkheads.
- Monitor timeouts and response times, and review values regularly.
17. Common Mistakes to Avoid
| Mistake | What goes wrong | Better approach |
|---|---|---|
| No timeout at all | One hung service blocks threads until the caller fails too | Set explicit timeouts everywhere |
| Only a connect timeout | A server that accepts but never answers still blocks the caller | Also set a read timeout |
| Timeouts longer than the caller’s | Inner work continues after the user has already given up | Make inner timeouts shorter |
| The same value for everything | Fast services wait too long; slow ones fail too often | Set values per dependency |
| Retrying with full timeouts each time | Total time far exceeds what users accept | Fit all attempts into one budget |
| Assuming a timeout means “not done” | Duplicate payments or orders after a retry | Use idempotency keys and check status |
orTimeout without client timeouts | Background work keeps running and piles up | Also set timeouts on the underlying client |
18. Summary
The Timeout pattern is the simplest and most important resilience technique in microservices. By setting a maximum wait for every remote operation, it stops slow or hung dependencies from tying up threads and spreading failures across the system. Configure connect, read and overall timeouts on HTTP clients, databases and gateways; base the values on measured response times; keep inner timeouts shorter than outer ones; and decide in advance what happens when time runs out. Combined with retries, circuit breakers, bulkheads and fallbacks, timeouts keep your services fast, responsive and resilient, even when parts of the system are struggling. Continue with the Circuit Breaker pattern to learn how to stop calling a service that keeps timing out.
19. Frequently Asked Questions (FAQ)
Q1. What is the Timeout pattern in simple words?
The Timeout pattern means deciding in advance how long you are willing to wait for an operation, such as a call to another service. If the answer does not arrive in time, you stop waiting and handle it, for example by showing a fallback or an error.
It stops one slow service from making everything else slow.
Q2. Why is a slow service worse than a service that is down?
A service that is down usually fails immediately: the connection is refused, the caller gets an error in milliseconds, and its resources are freed.
A slow or hung service keeps callers waiting, holding threads and connections. As more requests arrive, the callers run out of resources and become slow too. Timeouts turn a slow failure into a fast one.
Q3. What is the difference between a connect timeout and a read timeout?
- A connect timeout limits how long the client waits to open a connection to the server.
- A read timeout limits how long it waits for response data once connected.
You need both: a server can accept connections instantly and then never reply, which only a read timeout catches.
Q4. What are the default timeouts in Java HTTP clients?
Often none or very long. For example, the JDK’s HttpURLConnection and many older clients wait indefinitely by default unless you configure them, and the JDK’s HttpClient has no request timeout unless you set one.
Never rely on defaults: set timeouts explicitly for every client, ideally in one central configuration.
Q5. How do I choose the right timeout value?
- Measure the dependency’s response times, especially the 99th percentile.
- Set the timeout a bit above that, for example 1.5 times the p99.
- Make sure it fits within the caller’s own time budget.
- Watch the timeout rate in production and adjust.
If healthy requests regularly hit the limit, the service is too slow or the timeout is too tight.
Q6. Does a timeout cancel the work on the other service?
Usually no. The timeout only makes the caller stop waiting. The other service may still be processing the request and may even complete it successfully after the caller gave up.
That is why operations that change data need idempotency keys, and why the caller should check the real status before assuming the operation failed. gRPC deadlines are an exception: the server is told when the client gives up.
Q7. Should I retry after a timeout?
Only if it is safe and there is time left:
- Read operations can usually be retried.
- Operations that change data must be idempotent, for example by using an idempotency key.
- All attempts together must fit within the user’s time budget.
If a service keeps timing out, stop retrying and let a circuit breaker open.
Q8. What is a timeout budget?
A timeout budget is the total time allowed for a user request, shared between all the steps it needs. If the user should get an answer within 3 seconds, the gateway, the service and every downstream call must fit inside those 3 seconds.
The rule: inner timeouts are always shorter than outer ones, so no service keeps working on a request that has already been abandoned.
Q9. What is deadline propagation?
Deadline propagation means passing the remaining time from one service to the next. Instead of each service using a fixed timeout, it uses whatever time is actually left in the original request.
gRPC does this automatically: a deadline set by the client travels with the call, and downstream calls inherit what remains. With REST, you can send the deadline in a header and check it in each service.
Q10. What HTTP status code should a timeout return?
504 Gateway Timeout: when your service or gateway did not get an answer from a downstream service in time.503 Service Unavailable: when your service cannot handle the request right now, for example because a circuit breaker is open.408 Request Timeout: only when the client was too slow to send its request.
Q11. Is CompletableFuture.orTimeout enough on its own?
No. orTimeout completes the future with an exception after the time limit, so the caller stops waiting. But the background task keeps running, and if many calls time out this way, the stuck tasks can pile up.
Combine it with timeouts on the underlying client or driver, so the actual network call also ends.
Q12. What is the difference between Resilience4j TimeLimiter and client timeouts?
- Client timeouts, such as connect and read timeouts, stop the actual network operation.
- TimeLimiter limits how long the caller waits for a
CompletableFutureor reactive result, and integrates with fallbacks, retries and circuit breakers.
Use client timeouts always, and TimeLimiter when you want an overall limit around a whole asynchronous operation.
Q13. Do I need timeouts for database calls?
Yes. Databases can hang because of locks, missing indexes or exhausted connection pools. Set:
- A pool timeout, such as HikariCP’s
connection-timeout. - A query timeout, for example the JPA
jakarta.persistence.query.timeouthint. - A transaction timeout with
@Transactional(timeout = ...)where needed.
Q14. How do timeouts work with circuit breakers?
A circuit breaker counts timeouts and slow calls as failures. When too many calls in a row time out, it opens and fails new calls immediately, without waiting at all.
This saves even more time and resources than the timeout alone, and gives the struggling service a chance to recover. Resilience4j’s circuit breaker also has a slow call threshold for calls that succeed but take too long.
Q15. Should timeouts be the same for every service?
No. Different dependencies have very different normal response times: a cache lookup takes milliseconds, a payment provider may take a second or two, and a report several seconds.
Use sensible defaults for all clients, and override them for specific dependencies based on their measured behavior.
Q16. How do I handle long-running operations that legitimately take minutes?
Do not keep a synchronous request open for minutes. Instead:
- Accept the request and reply immediately with
202 Acceptedand a job ID. - Process the job in the background, for example from a message queue.
- Let the client check a status endpoint, or notify it when the job is done.
This keeps request timeouts short while still supporting long work.
Q17. Can a timeout be too short?
Yes. A timeout that is too tight fails healthy requests during normal slow moments, such as garbage collection pauses or cold caches. With retries, this can even increase load on the dependency.
Watch the timeout rate: if many timed-out calls would have succeeded a little later, raise the limit slightly or make the slow path faster.
Q18. Do message broker clients need timeouts?
Yes. Producers and consumers talk to the broker over the network too. For example, Kafka producers have settings such as request.timeout.ms and delivery.timeout.ms, and consumers have poll and session timeouts.
Check these settings, so a broker problem cannot block your request threads, especially if you publish events while handling a user request.
Q19. Can a service mesh handle timeouts for me?
Yes. In a service mesh such as Istio, you can set a timeout for calls to a service in its configuration, and the sidecar proxies enforce it without code changes.
It is still wise to keep timeouts in your application clients, because the application knows best what fallback to use, and timeouts should stay consistent with the user’s time budget. Avoid conflicting values in the mesh and the code.
Q20. How do I test timeout behavior?
- Use WireMock with a fixed delay longer than your timeout, and check that the client gives up in time and uses the fallback.
- Use Testcontainers with network tools such as Toxiproxy to simulate slow or broken connections.
- In load tests, slow down one dependency and confirm that the rest of the system stays responsive.