Skip to content
Home / Skills / Resilience / Timeout
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.


References