Spring Boot Resilience4j TimeLimiter Tutorial

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 TimeoutExceptionhandler, 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 use CompletableFuture.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.

Leave a Comment

Your email address will not be published. Required fields are marked *