RE
Timeout
Resilience core v1.0.0
Timeout Patterns
Overview
Timeouts prevent indefinite blocking on remote operations. Without timeouts, slow or unresponsive services can exhaust resources and cause cascading failures.
Key Concepts
Timeout Types
┌─────────────────────────────────────────────────────────────┐
│ Timeout Types │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Connection Timeout: │
│ Client ────▶ [connecting...] ────▶ Server │
│ Time to establish TCP connection │
│ Typical: 1-5 seconds │
│ │
│ 2. Read/Socket Timeout: │
│ Client ◀──── [waiting for data] ──── Server │
│ Time to receive next data packet │
│ Typical: 5-30 seconds │
│ │
│ 3. Request/Call Timeout: │
│ Client ────▶ [entire operation] ◀──── Server │
│ Total time for complete request/response │
│ Typical: 10-60 seconds │
│ │
│ 4. Deadline Propagation: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Client (deadline: 5s) │ │
│ │ │ │ │
│ │ ▼ (remaining: 5s) │ │
│ │ API Gateway ───────┐ │ │
│ │ │ │ │ │
│ │ ▼ (remaining: 4s)│ (remaining: 4s) │ │
│ │ Service A Service B │ │
│ │ │ │ │ │
│ │ ▼ (remaining: 3s)│ │ │
│ │ Database └──▶ External API │ │
│ │ │ │
│ │ All services respect remaining deadline │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Best Practices
1. Set Timeouts on Everything
Never use infinite timeouts on remote calls.
2. Use Deadline Propagation
Pass remaining time through the call chain.
3. Configure Connection vs Read Separately
Different failure modes need different timeouts.
4. Include Processing Headroom
Leave time for response processing.
5. Monitor Timeout Rates
Track timeout frequency per dependency.
Code Examples
Example 1: HTTP Client Timeouts
@Configuration
public class HttpClientConfig {
@Bean
public RestClient restClient() {
HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
return RestClient.builder()
.requestFactory(new JdkClientHttpRequestFactory(httpClient))
.requestInterceptor((request, body, execution) -> {
// Add request timeout header
request.getHeaders().add("X-Request-Timeout", "30000");
return execution.execute(request, body);
})
.build();
}
@Bean
public WebClient webClient() {
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.responseTimeout(Duration.ofSeconds(30))
.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(10, TimeUnit.SECONDS))
.addHandlerLast(new WriteTimeoutHandler(10, TimeUnit.SECONDS))
);
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
@Bean
public OkHttpClient okHttpClient() {
return new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.callTimeout(60, TimeUnit.SECONDS) // Total timeout
.build();
}
}
@Service
public class ExternalApiClient {
private final WebClient webClient;
public Mono<Response> callWithTimeout(Request request, Duration timeout) {
return webClient.post()
.uri("/api/resource")
.body(BodyInserters.fromValue(request))
.retrieve()
.bodyToMono(Response.class)
.timeout(timeout)
.onErrorMap(TimeoutException.class, e ->
new ServiceTimeoutException("External API timed out after " + timeout, e)
);
}
public Response callWithDeadline(Request request, Instant deadline) {
Duration remaining = Duration.between(Instant.now(), deadline);
if (remaining.isNegative() || remaining.isZero()) {
throw new DeadlineExceededException("Deadline already passed");
}
return callWithTimeout(request, remaining).block();
}
}
Example 2: Deadline Propagation
public class DeadlineContext {
private static final ThreadLocal<Instant> DEADLINE = new ThreadLocal<>();
public static void setDeadline(Instant deadline) {
DEADLINE.set(deadline);
}
public static Optional<Instant> getDeadline() {
return Optional.ofNullable(DEADLINE.get());
}
public static Duration getRemainingTime() {
return getDeadline()
.map(d -> Duration.between(Instant.now(), d))
.orElse(Duration.ofDays(1)); // No deadline = effectively infinite
}
public static void checkDeadline() {
if (isDeadlineExceeded()) {
throw new DeadlineExceededException("Request deadline exceeded");
}
}
public static boolean isDeadlineExceeded() {
return getDeadline()
.map(d -> Instant.now().isAfter(d))
.orElse(false);
}
public static void clear() {
DEADLINE.remove();
}
}
@Component
public class DeadlineFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// Extract deadline from header
String deadlineHeader = httpRequest.getHeader("X-Deadline");
if (deadlineHeader != null) {
Instant deadline = Instant.parse(deadlineHeader);
DeadlineContext.setDeadline(deadline);
} else {
// Set default deadline
Duration defaultTimeout = Duration.ofSeconds(30);
DeadlineContext.setDeadline(Instant.now().plus(defaultTimeout));
}
try {
DeadlineContext.checkDeadline();
chain.doFilter(request, response);
} finally {
DeadlineContext.clear();
}
}
}
@Component
public class DeadlineAwareRestClient {
private final RestClient restClient;
public <T> T call(String url, Object request, Class<T> responseType) {
// Check deadline before making call
DeadlineContext.checkDeadline();
Duration remaining = DeadlineContext.getRemainingTime();
// Reserve some time for processing
Duration callTimeout = remaining.minus(Duration.ofMillis(100));
if (callTimeout.isNegative()) {
throw new DeadlineExceededException("Insufficient time remaining for call");
}
return restClient.post()
.uri(url)
.header("X-Deadline", DeadlineContext.getDeadline()
.map(Instant::toString)
.orElse(null))
.body(request)
.retrieve()
.body(responseType);
}
}
Example 3: TimeLimiter Pattern
@Configuration
public class TimeLimiterConfig {
@Bean
public TimeLimiterRegistry timeLimiterRegistry() {
io.github.resilience4j.timelimiter.TimeLimiterConfig defaultConfig =
io.github.resilience4j.timelimiter.TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(10))
.cancelRunningFuture(true)
.build();
TimeLimiterRegistry registry = TimeLimiterRegistry.of(defaultConfig);
// Fast timeout for non-critical operations
registry.addConfiguration("fast",
io.github.resilience4j.timelimiter.TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(2))
.build()
);
// Longer timeout for batch operations
registry.addConfiguration("batch",
io.github.resilience4j.timelimiter.TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofMinutes(5))
.build()
);
return registry;
}
}
@Service
public class TimeoutProtectedService {
private final TimeLimiter timeLimiter;
private final ScheduledExecutorService scheduler;
public TimeoutProtectedService(TimeLimiterRegistry registry) {
this.timeLimiter = registry.timeLimiter("default");
this.scheduler = Executors.newScheduledThreadPool(4);
}
public Result executeWithTimeout(Callable<Result> operation) {
CompletableFuture<Result> future = CompletableFuture.supplyAsync(() -> {
try {
return operation.call();
} catch (Exception e) {
throw new CompletionException(e);
}
});
try {
return timeLimiter.executeFutureSupplier(() -> future);
} catch (TimeoutException e) {
future.cancel(true); // Attempt to cancel
throw new ServiceTimeoutException("Operation timed out", e);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* With fallback on timeout
*/
public Result executeWithFallback(
Callable<Result> operation,
Supplier<Result> fallback) {
try {
return executeWithTimeout(operation);
} catch (ServiceTimeoutException e) {
log.warn("Operation timed out, using fallback", e);
return fallback.get();
}
}
/**
* Async with timeout
*/
public CompletableFuture<Result> executeAsync(Callable<Result> operation) {
return TimeLimiter.decorateFutureSupplier(
timeLimiter,
scheduler,
() -> CompletableFuture.supplyAsync(() -> {
try {
return operation.call();
} catch (Exception e) {
throw new CompletionException(e);
}
})
).get();
}
}
Example 4: Database Timeout Configuration
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.hikari")
public HikariDataSource dataSource() {
HikariDataSource ds = new HikariDataSource();
// Connection timeout: time to wait for connection from pool
ds.setConnectionTimeout(5000); // 5 seconds
// Validation timeout: time for connection validation query
ds.setValidationTimeout(3000); // 3 seconds
// Max lifetime: maximum time a connection can exist
ds.setMaxLifetime(1800000); // 30 minutes
// Idle timeout: time before idle connection is removed
ds.setIdleTimeout(600000); // 10 minutes
// Socket timeout via JDBC URL or datasource properties
ds.addDataSourceProperty("socketTimeout", "30"); // 30 seconds
return ds;
}
}
@Repository
public class TimeoutAwareRepository {
private final JdbcTemplate jdbcTemplate;
public TimeoutAwareRepository(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
// Set query timeout
this.jdbcTemplate.setQueryTimeout(10); // 10 seconds
}
public List<Entity> findWithTimeout(String criteria, int timeoutSeconds) {
return jdbcTemplate.execute((Connection conn) -> {
try (PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM entities WHERE criteria = ?")) {
ps.setQueryTimeout(timeoutSeconds);
ps.setString(1, criteria);
try (ResultSet rs = ps.executeQuery()) {
return mapResults(rs);
}
}
});
}
/**
* Using JPA query hints
*/
@QueryHints({
@QueryHint(name = "javax.persistence.query.timeout", value = "10000")
})
List<Entity> findByCriteria(String criteria);
/**
* Propagate deadline to query timeout
*/
public List<Entity> findWithDeadline(String criteria) {
Duration remaining = DeadlineContext.getRemainingTime();
int timeoutSeconds = Math.max(1, (int) remaining.getSeconds() - 1);
return findWithTimeout(criteria, timeoutSeconds);
}
}
Example 5: Cascading Timeout Strategy
@Service
public class OrderProcessingService {
private final InventoryService inventoryService;
private final PaymentService paymentService;
private final ShippingService shippingService;
/**
* Process order with cascading timeouts
* Total budget distributed across steps
*/
public OrderResult processOrder(Order order, Duration totalBudget) {
Instant deadline = Instant.now().plus(totalBudget);
DeadlineContext.setDeadline(deadline);
try {
// Allocate time budget proportionally
// Inventory check: 20% of budget
// Payment: 50% of budget
// Shipping: 30% of budget
Duration inventoryTimeout = totalBudget.multipliedBy(20).dividedBy(100);
Duration paymentTimeout = totalBudget.multipliedBy(50).dividedBy(100);
Duration shippingTimeout = totalBudget.multipliedBy(30).dividedBy(100);
// Step 1: Check inventory
InventoryResult inventory = executeWithTimeout(
() -> inventoryService.reserve(order.getItems()),
inventoryTimeout,
"inventory check"
);
if (!inventory.isAvailable()) {
return OrderResult.outOfStock(inventory.getUnavailableItems());
}
try {
// Step 2: Process payment
PaymentResult payment = executeWithTimeout(
() -> paymentService.charge(order.getPaymentDetails()),
adjustTimeout(paymentTimeout),
"payment processing"
);
if (!payment.isSuccessful()) {
inventoryService.release(order.getItems());
return OrderResult.paymentFailed(payment.getError());
}
// Step 3: Create shipment
ShipmentResult shipment = executeWithTimeout(
() -> shippingService.createShipment(order),
adjustTimeout(shippingTimeout),
"shipment creation"
);
return OrderResult.success(order.getId(), shipment.getTrackingNumber());
} catch (TimeoutException e) {
// Compensate: release inventory
inventoryService.release(order.getItems());
throw e;
}
} finally {
DeadlineContext.clear();
}
}
private <T> T executeWithTimeout(
Callable<T> operation,
Duration timeout,
String operationName) throws TimeoutException {
// Check if we still have time
DeadlineContext.checkDeadline();
Duration remaining = DeadlineContext.getRemainingTime();
Duration actualTimeout = timeout.compareTo(remaining) < 0 ? timeout : remaining;
log.debug("Executing {} with timeout {}ms", operationName, actualTimeout.toMillis());
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<T> future = executor.submit(operation);
try {
return future.get(actualTimeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (java.util.concurrent.TimeoutException e) {
future.cancel(true);
throw new TimeoutException(operationName + " timed out after " + actualTimeout);
} catch (ExecutionException e) {
throw new RuntimeException(e.getCause());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} finally {
executor.shutdownNow();
}
}
private Duration adjustTimeout(Duration allocated) {
Duration remaining = DeadlineContext.getRemainingTime();
// Use smaller of allocated and remaining
if (remaining.compareTo(allocated) < 0) {
log.warn("Reducing timeout from {}ms to {}ms due to deadline",
allocated.toMillis(), remaining.toMillis());
return remaining;
}
return allocated;
}
}
Anti-Patterns
❌ No Timeout
// WRONG - can hang forever
Response response = httpClient.send(request);
// ✅ CORRECT - always set timeout
Response response = httpClient.send(request)
.timeout(Duration.ofSeconds(30));
❌ Same Timeout for All Operations
Different operations need different timeouts based on expected latency.