00:00

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.

How one slow service spreads
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

TimeoutWhat it limitsTypical value inside a data center
Connect timeoutTime to open a network connection to the server0.5–2 s
Read (response) timeoutTime to wait for response data once connected1–5 s, based on the service
Connection pool timeoutTime to wait for a free connection from a pool0.5–3 s
Overall (total) timeoutTotal time for the whole operation, including retriesWithin the user’s time budget
Idle timeoutHow long an unused connection stays openTens of seconds to minutes
Query / transaction timeoutMaximum time for a database query or transaction1–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

Timeline of a call with a 2-second timeout
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

  1. The payment service hits a database lock and stops responding.
  2. Every order request now waits, possibly for minutes.
  3. Within seconds, all of the order service’s request threads are waiting.
  4. The order service stops answering any request, including “view my orders”, which does not need payments at all.

5.2 With a timeout

  1. The order service waits at most 2 seconds for the payment service.
  2. After 2 seconds, it stops waiting and tells the user: “We could not confirm your payment yet. Please try again in a moment.”
  3. Threads are freed quickly, so other features keep working.
  4. 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:

HttpTimeoutConfig.java
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:

WebClientConfig.java
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

application.yml
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:

TimeoutExamples.java
// 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);
MethodOn timeoutUse for
orTimeout(time, unit)Completes the future with a TimeoutExceptionRequired data, where the caller must react to the failure
completeOnTimeout(value, time, unit)Completes the future with the given default valueOptional data, such as recommendations or reviews
Important: These methods stop the caller from waiting, but they do not stop the background task, which may keep running. Always combine them with timeouts on the underlying client, so the work itself also ends.

8. Timeouts with Resilience4j TimeLimiter

Resilience4j’s TimeLimiter adds timeouts declaratively and works well with its retry and circuit breaker modules:

application.yml
resilience4j:
  timelimiter:
    instances:
      shipping:
        timeout-duration: 2s
        cancel-running-future: true   # try to cancel the work when time is up
ShippingQuoteService.java
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:

application.properties
# 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
ReportService.java
@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:

LayerSettingPurpose
API gateway (Spring Cloud Gateway)spring.cloud.gateway.server.webflux.httpclient.response-timeoutStop waiting for slow backend services
Embedded Tomcatserver.tomcat.connection-timeoutClose connections from clients that send data too slowly
Spring MVC async requestsspring.mvc.async.request-timeoutLimit asynchronous request processing
Service mesh (Istio)timeout in a VirtualServiceEnforce a timeout on every call, without code changes
Load balancerIdle / request timeout settingsClose stuck connections at the edge
application.yml (gateway)
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:

  1. Measure the dependency’s response times, especially the 99th percentile (p99): the time within which 99% of requests finish.
  2. Set the read timeout a little above the p99, for example p99 × 1.5, so normal slow requests still succeed.
  3. Check that the value fits inside the caller’s own time budget (section 12).
  4. Review the values when traffic or code changes.
DependencyMeasured p99Suggested read timeout
Inventory lookup120 ms200–300 ms
Payment provider1.2 s2 s
Product search400 ms600–800 ms
Monthly report (background)40 s60 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:

A 3-second budget for one user request
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:

Deadline.java
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();
    }
}
OrderService.java (using a deadline)
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:

OptionWhen to use itWatch out for
Return a fallbackOptional data: recommendations, reviews, estimatesMake it clear to users when data is incomplete
RetryIdempotent operations, such as reading dataOnly within the remaining time budget; use backoff
Report “pending”Important actions that may have succeeded, such as paymentsCheck the real status later, or let the other service notify you
Return an errorWhen nothing useful can be shownUse 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

PatternHow it works with timeouts
RetryA timed-out call can be retried, if safe; each attempt has its own timeout, and all attempts fit in the total budget
Circuit breakerCounts timeouts and slow calls as failures; when too many occur, it stops calling the service for a while
BulkheadLimits how many calls can wait at the same time, so even short timeouts cannot use up every thread
FallbackGives 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

  1. Set a timeout on every remote call: HTTP, gRPC, database, cache and message broker clients.
  2. Never rely on defaults; many are very long or infinite.
  3. Set both connect and read timeouts, plus an overall limit for user requests.
  4. Base values on measured percentiles, not guesses.
  5. Make inner timeouts shorter than outer ones, and keep everything within the user’s time budget.
  6. Configure timeouts centrally, with per-dependency overrides where needed.
  7. Plan the reaction: fallback, safe retry, “pending” status or a clear error.
  8. Use idempotency keys for operations that change data.
  9. Combine with circuit breakers and bulkheads.
  10. Monitor timeouts and response times, and review values regularly.

17. Common Mistakes to Avoid

MistakeWhat goes wrongBetter approach
No timeout at allOne hung service blocks threads until the caller fails tooSet explicit timeouts everywhere
Only a connect timeoutA server that accepts but never answers still blocks the callerAlso set a read timeout
Timeouts longer than the caller’sInner work continues after the user has already given upMake inner timeouts shorter
The same value for everythingFast services wait too long; slow ones fail too oftenSet values per dependency
Retrying with full timeouts each timeTotal time far exceeds what users acceptFit all attempts into one budget
Assuming a timeout means “not done”Duplicate payments or orders after a retryUse idempotency keys and check status
orTimeout without client timeoutsBackground work keeps running and piles upAlso 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.