Resilience4j is the TimeLimiter module, which helps prevent system slowdowns by setting time constraints on method executions. In this article, we’ll explore the @TimeLimiter
annotation, its practical use cases, and how to configure it effectively in a Spring Boot application.
1. What Is Resilience4j TimeLimiter?
Resilience4j’s TimeLimiter is a module that enforces time limits on method executions, particularly those that return a CompletableFuture
. If a method takes longer than the configured time limit, the system will throw a TimeoutException
handler, preventing excessive delays from cascading through the system.
Why Is Time Limiting Important?
- Prevents slow responses: It ensures that requests are completed within the expected time or fail fast, allowing the system to recover.
- Avoids resource exhaustion: A slow API response can lead to resource exhaustion if multiple threads wait indefinitely.
- Enhances system stability: We can avoid failures propagating across dependent services by limiting response times.
2. Adding Resilience4j TimeLimiter to a Spring Boot Application
To use the @TimeLimiter
annotation, we need to add the required dependencies and configure our application. Let’s go step by step.
Step 1: Add Dependencies
If you’re using Maven, include the following dependencies in your pom.xml
:
<dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot3</artifactId> <version>2.2.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
Step 2: Enable Resilience4j in Your Spring Boot App
Add the @EnableAspectJAutoProxy
annotation to your main Spring Boot class to enable Spring AOP (Aspect-Oriented Programming), which is required for Resilience4j annotations to work correctly.
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.EnableAspectJAutoProxy; @SpringBootApplication @EnableAspectJAutoProxy public class Resilience4jDemoApplication { public static void main(String[] args) { SpringApplication.run(Resilience4jDemoApplication.class, args); } }
3. Using @TimeLimiter
in a Spring Boot Service
Now, let’s create a service method and apply the @TimeLimiter
annotation.
Example: Simulating a Delayed API Call
import io.github.resilience4j.timelimiter.annotation.TimeLimiter; import org.springframework.stereotype.Service; import java.time.Duration; import java.util.concurrent.CompletableFuture; @Service public class TimeLimiterService { @TimeLimiter(name = "backendService", fallbackMethod = "fallbackResponse") public CompletableFuture<String> slowService() { return CompletableFuture.supplyAsync(() -> { try { Thread.sleep(5000); // Simulating a long-running task } catch (InterruptedException e) { throw new RuntimeException(e); } return "Response from slow service"; }); } public CompletableFuture<String> fallbackResponse(Throwable t) { return CompletableFuture.completedFuture("Fallback response due to timeout"); } }
Explanation:
1. @TimeLimiter(name = "backendService", fallbackMethod = "fallbackResponse")
- This annotation applies the TimeLimiter to the
slowService
method. - If the method execution exceeds the configured time limit, the fallback method (
fallbackResponse
) is executed.
2. CompletableFuture<String> slowService()
- This method simulates a delayed API call using
Thread.sleep(5000)
, which means it takes 5 seconds to execute. - Since TimeLimiter only works with methods returning
CompletableFuture
, we useCompletableFuture.supplyAsync()
to make it non-blocking.
3. fallbackResponse(Throwable t)
- If a timeout occurs, this method provides a graceful fallback response instead of propagating the failure.
4. Configuring Time Limits in application.yml
The timeout duration for the @TimeLimiter
can be configured in application.yml
or application.properties
under the resilience4j.timelimiter
section.
resilience4j: timelimiter: instances: backendService: timeout-duration: 2s
Explanation:
backendService.timeout-duration: 2s
→ Limits the execution time to 2 seconds. If the method takes longer than 2 seconds, it will fail fast and return the fallback response.
5. Testing the @TimeLimiter
Annotation
To test the behavior of @TimeLimiter
annotation, let’s create a simple REST controller:
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.concurrent.CompletableFuture; @RestController @RequestMapping("/api") public class TimeLimiterController { private final TimeLimiterService timeLimiterService; public TimeLimiterController(TimeLimiterService timeLimiterService) { this.timeLimiterService = timeLimiterService; } @GetMapping("/slow") public CompletableFuture<String> getSlowResponse() { return timeLimiterService.slowService(); } }
Expected Results:
- Request Duration < 2 seconds, Outcome “Response from slow service”
- Request Duration > 2 seconds, Outcome “Fallback response due to timeout”
The developer can use Windows PowerShell to test the application.
5.1 Test case Response from slow service
Change resilience4j configuration
resilience4j: timelimiter: instances: backendService: timeout-duration: 10s
curl http://localhost:8080/api/slow
Response:
StatusCode : 200 StatusDescription : Content : Response from slow service RawContent : HTTP/1.1 200 Keep-Alive: timeout=60 Connection: keep-alive Content-Length: 26 Content-Type: text/plain;charset=UTF-8 Date: Thu, 13 Feb 2025 07:47:22 GMT Response from slow service Forms : {} Headers : {[Keep-Alive, timeout=60], [Connection, keep-alive], [Content-Length, 26], [Content-Type, text/plain;charset=UTF-8]...} Images : {} InputFields : {} Links : {} ParsedHtml : mshtml.HTMLDocumentClass RawContentLength : 26
5.2 Test case Fallback response due to timeout
Change resilience4j configuration
resilience4j: timelimiter: instances: backendService: timeout-duration: 2s
curl http://localhost:8080/api/slow
Response:
StatusCode : 200 StatusDescription : Content : Fallback response due to timeout RawContent : HTTP/1.1 200 Keep-Alive: timeout=60 Connection: keep-alive Content-Length: 32 Content-Type: text/plain;charset=UTF-8 Date: Thu, 13 Feb 2025 07:51:39 GMT Fallback response due to timeout Forms : {} Headers : {[Keep-Alive, timeout=60], [Connection, keep-alive], [Content-Length, 32], [Content-Type, text/plain;charset=UTF-8]...} Images : {} InputFields : {} Links : {} ParsedHtml : mshtml.HTMLDocumentClass RawContentLength : 32
6. Handling Timeout Exceptions
If developers don’t provide a fallback method, Resilience4j will throw a TimeoutException
, which can be handled using a global exception handler:
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import java.util.concurrent.TimeoutException; @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(TimeoutException.class) public String handleTimeoutException(TimeoutException ex) { return "Request timed out. Please try again later."; } }
6.1 Test case Fallback response due to timeout
If the developer wants to use a global exception. The developer must remove the fallback argument from the TimeLimiter annotation.
@TimeLimiter(name = "backendService") public CompletableFuture<String> slowService() { return CompletableFuture.supplyAsync(() -> { try { Thread.sleep(5000); // Simulating a long-running task } catch (InterruptedException e) { throw new RuntimeException(e); } return "Response from slow service"; }); }
curl http://localhost:8080/api/slow
Response:
StatusCode : 200 StatusDescription : Content : Request timed out. Please try again later. RawContent : HTTP/1.1 200 Keep-Alive: timeout=60 Connection: keep-alive Content-Length: 42 Content-Type: text/plain;charset=UTF-8 Date: Thu, 13 Feb 2025 07:56:51 GMT Request timed out. Please try again la... Forms : {} Headers : {[Keep-Alive, timeout=60], [Connection, keep-alive], [Content-Length, 42], [Content-Type, text/plain;charset=UTF-8]...} Images : {} InputFields : {} Links : {} ParsedHtml : mshtml.HTMLDocumentClass RawContentLength : 42
Response content from the Spring Boot application returned from the GlobalExceptionHandler Class.
7. Combining @TimeLimiter
with Other Resilience4j Annotations
In real-world applications, developers can combine @TimeLimiter
with other Resilience4j annotations for better resilience:
@Retry
→ Automatically retries failed requests.@CircuitBreaker
→ Opens the circuit if a service repeatedly fails.@RateLimiter
→ Limits the number of requests per second.
Example:
@TimeLimiter(name = "backendService", fallbackMethod = "fallbackResponse") @CircuitBreaker(name = "backendService", fallbackMethod = "fallbackResponse") @Retry(name = "backendService") public CompletableFuture<String> resilientService() { // Service logic }
Finally
Resilience4j @TimeLimiter
Annotation is essential for preventing slow responses from bringing down your system. By defining strict execution time limits and providing fallback responses, you can improve your application’s stability, scalability, and user experience.
Integrating with Circuit Breakers and Retries will help developers build a highly resilient and fault-tolerant application when working with microservices or distributed systems.