Migration & What’s Changed

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Most services that need this page arrive from one of four places: a Hystrix command that still runs in production, a Resilience4j 1.x dependency that was never upgraded, a Spring Boot 2 or 3 application that is moving to Boot 4, or a @Retryable method that is still on the archived Spring Retry project. Each has a section below with a mapping table, a before and after example and the traps found while running it. The page ends with the list of places where the official readme.io documentation disagrees with the code, collected from the whole section so that you can see them in one place.

The page was written against Resilience4j 2.4.0, Spring Boot 4.1.x (4.1.1), Spring Framework 7.0.x (7.0.9) and Java 21, and every example was compiled and run on exactly those versions on 4 October 2026. Where the old stack is shown it is the version that was current for it: Hystrix 1.5.18, Spring Boot 2.7.18 with Resilience4j 2.3.0, Spring Boot 3.5.16 with Resilience4j 2.4.0, and Spring Retry 2.0.13. The vocabulary (breaker, bulkhead, retry storm) is in Resilience Fundamentals.

Choosing a migration path

flowchart TD S["What do you run today?"] --> H{"Hystrix or
Spring Cloud Netflix?"} H -- yes --> H1["Hystrix to Resilience4j
(concept and property tables)"] H1 --> R H -- no --> R{"Resilience4j 1.x?"} R -- yes --> R1["1.x to 2.x
(Java 17, Vavr, removed keys)"] R1 --> B R -- no --> B{"Spring Boot version?"} B -- "2.x" --> B2["spring-boot2 stops at 2.3.0
move to Boot 3 first"] B2 --> B3 B -- "3.x" --> B3["resilience4j-spring-boot3 2.4.0
works on Boot 3.5"] B3 --> B4["Boot 4: resilience4j-spring-boot4
+ spring-boot-starter-aspectj"] B -- "4.x" --> B4 B4 --> SR{"Spring Retry
@Retryable?"} SR -- yes --> SR1["Spring Framework 7 @Retryable
or Resilience4j Retry"] SR -- no --> D["Done: check the readme.io table
before trusting the old docs"] SR1 --> D
Figure 1. Which migration applies to you

The order matters. Hystrix and Resilience4j 1.x are library migrations that you can do on the Spring Boot version you already have, and each is easier to test on a stable platform. The Boot 4 step changes the platform (Spring Framework 7, Jakarta EE 11, new starters), so it is better done last, on code that already speaks Resilience4j 2.x.

timeline title Resilience4j and the Spring stack 2.0.0 : Vavr removed : Java 17 baseline : Spring Boot 2.7 2.0.1 : resilience4j-spring6 and -spring-boot3 : deprecated keys removed 2.1.0 : recordResultPredicate in Boot properties 2.3.0 : last spring-boot2 and spring-cloud2 : lock-free sliding window : ReentrantLock 2.4.0 : resilience4j-spring-boot4 : initialState : version verifier
Figure 2. Milestones of the Resilience4j line that matter for a migration

Source: RELEASENOTES.adoc at v2.4.0 for 2.0.0 to 2.3.0, and the GitHub release of v2.4.0 for 2.4.0, because the notes file at the 2.4.0 tag stops at 2.3.0.

Hystrix to Resilience4j

Hystrix is in maintenance mode, with 1.5.18 as its last release, and its Spring integration (Spring Cloud Netflix Hystrix) is not part of the current Spring Cloud release train, whose circuit breaker support is Spring Cloud CircuitBreaker. The history and the design differences are in Resilience Fundamentals; this section is the mapping. The mapping is conceptual first: Hystrix is one command object that bundles a circuit breaker, an isolation strategy, a timeout and a fallback, and Resilience4j is five separate decorators that you combine.

Concepts

Hystrix Resilience4j Where it is covered

HystrixCommand subclass, run()

A decorated function or an annotated method. run() is the body of the method, or the Supplier that you decorate.

Programmatic Usage, Annotations & Aspect Order

@HystrixCommand (Javanica) on a method

@CircuitBreaker, @Retry, @RateLimiter, @Bulkhead and @TimeLimiter on the method, each with its own name

Annotations & Aspect Order

getFallback(), fallbackMethod

fallbackMethod on each annotation, Decorators.ofSupplier(…​).withFallback(…​) in code

Fallbacks & Composition

Group key, command key

The instance name: one CircuitBreaker per remote dependency, created from resilience4j.circuitbreaker.instances.<name>

Registries, Configuration & Decorators

Thread pool key, THREAD isolation

A ThreadPoolBulkhead (@Bulkhead(type = THREADPOOL)), plus a TimeLimiter for the timeout

Bulkhead, TimeLimiter

SEMAPHORE isolation

The semaphore Bulkhead (maxConcurrentCalls)

Bulkhead

Execution timeout

TimeLimiter. It works on a CompletionStage or on a reactive type, not on a plain blocking method; for a blocking call set the timeout on the HTTP client.

TimeLimiter

Circuit breaker inside the command

CircuitBreaker

Circuit Breaker

execute(), queue(), observe(), toObservable()

A direct call; a CompletableFuture (with the thread-pool bulkhead and time limiter); a Mono, Flux or RxJava type (with resilience4j-reactor or -rxjava3)

Reactive, Kotlin & RxJava

HystrixBadRequestException (counts as neither success nor failure)

ignoreExceptions

Circuit Breaker

Retry (Hystrix has none; callers wrote their own)

Retry, with fixed, exponential and randomized back-off

Retry

Rate limiting (none)

RateLimiter

Rate Limiter

HystrixRequestCache, HystrixCollapser

There is no collapser. The Cache module is a result cache over a JCache, not a per-request cache.

Cache & Hedge

HystrixRequestContext, thread-local propagation

ContextPropagator on the thread-pool bulkhead

Observability

Hystrix Dashboard, Turbine, hystrix.stream

Actuator endpoints, Micrometer meters, Prometheus and Grafana

Observability

Archaius dynamic properties

Spring Environment binding, @RefreshScope registries on the Boot 4 starter, and the circuitbreakers write operation

Spring Boot Configuration

Source: Resilience4j: Comparison to Netflix Hystrix, Hystrix: How it Works and the sections of this documentation linked in the table.

Properties

The defaults of the Hystrix column were printed from a running Hystrix 1.5.18 command (HystrixCommandProperties and HystrixThreadPoolProperties); the Resilience4j defaults are the 2.4.0 constants. In Hystrix, properties are looked up as hystrix.command.<commandKey>.<name> with a default fallback; the Resilience4j equivalent is an entry under instances.<name> that inherits configs.default.

Hystrix property Default Resilience4j property (module) Default

execution.isolation.strategy

THREAD

Choose the bulkhead type: Bulkhead (semaphore) or ThreadPoolBulkhead

-

execution.isolation.semaphore.maxConcurrentRequests

10

maxConcurrentCalls (Bulkhead)

25

execution.isolation.thread.timeoutInMilliseconds, execution.timeout.enabled

1000 ms, enabled

timeoutDuration (TimeLimiter); no instance means no timeout

1 s

execution.isolation.thread.interruptOnTimeout

true

cancelRunningFuture (TimeLimiter)

true

fallback.enabled

true

A fallbackMethod on the annotation, or withFallback in code

none

fallback.isolation.semaphore.maxConcurrentRequests

10

No equivalent: a fallback runs on the calling thread, unbounded

-

circuitBreaker.enabled

true

Not applying the decorator, or the DISABLED state at run time

CLOSED

circuitBreaker.requestVolumeThreshold

20

minimumNumberOfCalls (CircuitBreaker)

100

circuitBreaker.errorThresholdPercentage

50

failureRateThreshold (CircuitBreaker), and the new slowCallRateThreshold

50, and 100

circuitBreaker.sleepWindowInMilliseconds

5000 ms

waitDurationInOpenState (CircuitBreaker)

60 s

circuitBreaker.forceOpen

false

The FORCED_OPEN state (transitionToForcedOpenState(), or the FORCE_OPEN write operation)

-

circuitBreaker.forceClosed

false

The DISABLED state (all calls pass, no metrics)

-

metrics.rollingStats.timeInMilliseconds, numBuckets

10000 ms, 10

slidingWindowType: TIME_BASED with slidingWindowSize in seconds (default window is COUNT_BASED with 100 calls)

COUNT_BASED, 100

metrics.healthSnapshot.intervalInMilliseconds

500 ms

None: the failure rate is evaluated on every recorded call

-

Half-open: one trial request

1

permittedNumberOfCallsInHalfOpenState (CircuitBreaker)

10

metrics.rollingPercentile.*

enabled

None: use the Micrometer timers

-

requestCache.enabled, requestLog.enabled

true

None

-

hystrix.threadpool.<key>.coreSize, maximumSize

10, 10

coreThreadPoolSize, maxThreadPoolSize (ThreadPoolBulkhead)

max(CPUs - 1, 1), CPUs

maxQueueSize (-1 = SynchronousQueue)

-1

queueCapacity (ThreadPoolBulkhead); 0 is a SynchronousQueue

100

queueSizeRejectionThreshold

5

None: the queue rejects when it is full

-

keepAliveTimeMinutes

1 minute

keepAliveDuration (ThreadPoolBulkhead)

20 ms

Source: Hystrix: Configuration for the property names, the printed defaults of Hystrix 1.5.18, and the v2.4.0 source of CircuitBreakerConfig, BulkheadConfig, ThreadPoolBulkheadConfig and TimeLimiterConfig.

The defaults are not the same, and that is where a migration changes behaviour without anyone touching the code. A Hystrix circuit with 20 requests in ten seconds is a Resilience4j breaker that, left on its defaults, needs 100 calls and stays open for 60 seconds. Set the Hystrix values explicitly to keep the old behaviour, as the example does, and then decide what you really want.

Behavioural differences that bite

  • Fallbacks. Hystrix runs the fallback on any failure, a timeout or a rejection. Resilience4j runs the fallbackMethod of each annotation when that decorator throws, so a fallback on the inner decorator hides the failure from the outer one. See Fallbacks & Composition.

  • Timeouts need a future. There is no blocking-method timeout in @TimeLimiter; a Hystrix command timed out a blocking call by running it on another thread, and Resilience4j gives that to the thread-pool bulkhead plus the time limiter.

  • Half-open. Hystrix lets one request through after the sleep window; Resilience4j lets permittedNumberOfCallsInHalfOpenState calls through (10 by default) and compares their failure rate with the threshold.

  • Slow calls. A Resilience4j breaker can open on slow calls (slowCallDurationThreshold, default 60 s) even when nothing fails; Hystrix reacts only to errors and timeouts.

  • Nothing is global. There is no hystrix.command.default that every call inherits unless you define configs.default, and an instance name that is not configured takes configs.default, not Resilience4j’s built-in defaults (see Spring Boot Configuration).

  • Metrics changed names. The Hystrix stream and the Hystrix dashboard have no counterpart; the meters and endpoints are in Observability.

The Bookshelf pricing call, before and after

The first block is a Hystrix command with the values of the table set explicitly. The second is the Resilience4j version of the same dependency protection: one annotated method, with the breaker and a semaphore bulkhead, and the configuration in YAML. Both were run against a gateway that fails every call.

Before: a Hystrix command (Hystrix 1.5.18)
package com.example.bookshelf;

class PriceCommand extends HystrixCommand<Long> {
    private final String isbn;

    PriceCommand(String isbn) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("pricing"))
            .andCommandKey(HystrixCommandKey.Factory.asKey("price"))
            .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
                .withExecutionIsolationSemaphoreMaxConcurrentRequests(10)
                .withExecutionTimeoutInMilliseconds(1000)
                .withCircuitBreakerRequestVolumeThreshold(20)
                .withCircuitBreakerErrorThresholdPercentage(50)
                .withCircuitBreakerSleepWindowInMilliseconds(5000)
                .withMetricsRollingStatisticalWindowInMilliseconds(10000)));
        this.isbn = isbn;
    }

    @Override protected Long run() { return pricing.priceCents(isbn); }

    @Override protected Long getFallback() { return 0L; }
}

long cents = new PriceCommand("978-0-13-468599-1").execute();

Source: Hystrix: Configuration and the HystrixCommand API of 1.5.18. Run: the first call returned the fallback 0; after 20 failed calls isCircuitBreakerOpen() was true, the next call was short-circuited, and the backend had been invoked 21 times in total.

After: an annotated method (Resilience4j 2.4.0)
package com.example.bookshelf;

import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.stereotype.Service;

@Service
public class PricingService {
    private final PricingGateway gateway;

    public PricingService(PricingGateway gateway) {
        this.gateway = gateway;
    }

    @CircuitBreaker(name = "pricing", fallbackMethod = "noPrice")
    @Bulkhead(name = "pricing")
    public long priceCents(String isbn) {
        return gateway.priceCents(isbn);
    }

    long noPrice(String isbn, Throwable cause) {
        return 0L;
    }
}
After: the Hystrix values as Resilience4j properties
resilience4j:
  circuitbreaker:
    instances:
      pricing:
        failureRateThreshold: 50        # errorThresholdPercentage
        minimumNumberOfCalls: 20        # requestVolumeThreshold
        slidingWindowType: TIME_BASED   # rollingStats.timeInMilliseconds
        slidingWindowSize: 10           # seconds
        waitDurationInOpenState: 5s     # sleepWindowInMilliseconds
  bulkhead:
    instances:
      pricing:
        maxConcurrentCalls: 10          # semaphore maxConcurrentRequests

Source: Resilience4j: Spring Boot 2 & 3 for the annotations and the property names, checked against the v2.4.0 CircuitBreakerConfigurationProperties and BulkheadConfigurationProperties. Run on Spring Boot 4.1.1: the first call returned the fallback 0, after 20 failed calls the breaker was OPEN with a failure rate of 100.0 and the backend had been invoked 20 times, and the next call returned the fallback without invoking the backend. A second test read the configuration back (20 calls, 50 percent, TIME_BASED, a window of 10, 10 permitted half-open calls, 10 concurrent calls). Note that the half-open value stayed at the Resilience4j default of 10, as the table says.

The timeout is the part that moved: Hystrix applied executionTimeoutInMilliseconds to every command, and the annotated method above has none. Put the timeout on the HTTP client (spring.http.clients.connect-timeout and read-timeout on Spring Boot 4), or make the method return a CompletionStage and add @TimeLimiter; both are shown in TimeLimiter.

Resilience4j 1.x to 2.x

Resilience4j 2.0.0 was a deliberate break; its release notes list four things: Vavr removed as a dependency, Java 17 as the baseline, Kotlin 1.7.20 and Spring Boot 2.7. 2.0.1 added Spring Boot 3 and removed the deprecated configuration keys.

Change What to do Version

Java 17 baseline

Build and run on Java 17 or later. The 2.4.0 jars read back as class-file major version 61, which is Java 17, so Java 21 and virtual threads are fine, and Java 11 and 8 are not.

2.0.0

Vavr removed from the core modules

Remove io.vavr imports from the decorator code. Try and Function1 based helpers are gone; the checked function types are in io.github.resilience4j.core.functions (CheckedFunction, CheckedSupplier, Either). Use plain try/catch, Optional or the Decorators builder. An optional resilience4j-vavr module is still listed in the 2.4.0 BOM for code that wants Vavr types.

2.0.0

ringBufferSizeInClosedState removed

Use slidingWindowSize

2.0.1

ringBufferSizeInHalfOpenState removed

Use permittedNumberOfCallsInHalfOpenState

2.0.1

Retry maxRetryAttempts removed

Use maxAttempts (it counts the first call too)

2.0.1

Bulkhead contextPropagator (list) removed

Use contextPropagators

2.0.1

resilience4j-spring-boot3 and resilience4j-spring6 added

Use them for Spring Boot 3 and Spring Framework 6

2.0.1

recordResultPredicate configurable from Boot properties

Available from YAML

2.1.0

Apache Commons Configuration registries

resilience4j-commons-configuration

2.2.0

Source: RELEASENOTES.adoc at v2.4.0, sections 2.0.0, 2.0.1, 2.1.0 and 2.2.0, and the bytecode version of the 2.4.0 jars.

The removed keys are the dangerous part, because a Spring Boot application does not reject an unknown key: it ignores it. The test below binds the 1.x spelling and the 2.x spelling side by side:

The same four settings in the 1.x and the 2.x spelling
@SpringBootTest(classes = BookshelfApplication.class, properties = {
    "resilience4j.circuitbreaker.instances.legacy.ringBufferSizeInClosedState=4",
    "resilience4j.circuitbreaker.instances.legacy.ringBufferSizeInHalfOpenState=2",
    "resilience4j.retry.instances.legacy.maxRetryAttempts=7",
    "resilience4j.circuitbreaker.instances.current.slidingWindowSize=4",
    "resilience4j.circuitbreaker.instances.current.permittedNumberOfCallsInHalfOpenState=2",
    "resilience4j.retry.instances.current.maxAttempts=7"
})
class RenamedPropertiesTest {
    // legacy:  slidingWindowSize 100, half-open 10, maxAttempts 3   (the 1.x keys did nothing)
    // current: slidingWindowSize 4,   half-open 2,  maxAttempts 7
}

Source: RELEASENOTES.adoc at v2.4.0 (2.0.1, "Removed deprecated configuration properties"). Run on Spring Boot 4.1.1 and Resilience4j 2.4.0: the instance with the 1.x keys read back a sliding window of 100, 10 half-open calls and 3 attempts, which are the defaults; the instance with the 2.x keys read back 4, 2 and 7. A service migrated without a search for these key names keeps running, with a breaker that waits for 100 calls instead of 4. Search the configuration of every environment, including Config Server and Kubernetes ConfigMaps.

After the key names, the code differences are small: Decorators, the registries, the event consumers and the aspects behave as before. The things that changed in the Spring modules over the 2.x line are listed in What changed in 2.3.0 and 2.4.0.

Spring Boot 2 to 3 to 4

The resilience4j.* property keys are identical on all three Boot lines. What changes is the starter, the AOP starter, the platform and a few packages.

Spring Boot Resilience4j starter AOP dependency for the annotations Last Resilience4j release

2.x (Framework 5)

resilience4j-spring-boot2 (pulls resilience4j-spring)

spring-boot-starter-aop

2.3.0; the module is not built from the 2.4.0 sources

3.x (Framework 6)

resilience4j-spring-boot3 (pulls resilience4j-spring6)

spring-boot-starter-aop

2.4.0 (verified on Boot 3.5.16)

4.x (Framework 7)

resilience4j-spring-boot4 (pulls resilience4j-spring6)

spring-boot-starter-aspectj

2.4.0

Source: Maven Central metadata and the v2.4.0 settings.gradle, and mvn dependency:tree runs of the three poms below. The 2.4.0 sources still contain the -spring, -spring-boot2 and -spring-cloud2 directories, but settings.gradle includes only spring6, spring-boot3 and spring-boot4; the three legacy modules were last published in 2.3.0.

Before and after, in Maven

Spring Boot 2.7 with Resilience4j 2.3.0 (the last release of this starter)
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.7.18</version>
</parent>
<dependencies>
  <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
  <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>
  <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
  <dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot2</artifactId>
    <version>2.3.0</version>
  </dependency>
</dependencies>
Spring Boot 3.5 with Resilience4j 2.4.0
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.5.16</version>
</parent>
<properties>
  <resilience4j.version>2.4.0</resilience4j.version>
</properties>
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>io.github.resilience4j</groupId>
      <artifactId>resilience4j-bom</artifactId>
      <version>${resilience4j.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
<dependencies>
  <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
  <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>
  <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
  <dependency><groupId>io.github.resilience4j</groupId><artifactId>resilience4j-spring-boot3</artifactId></dependency>
</dependencies>
Spring Boot 4.1 with Resilience4j 2.4.0
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>4.1.1</version>
</parent>
<properties>
  <java.version>21</java.version>
  <resilience4j.version>2.4.0</resilience4j.version>
</properties>
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>io.github.resilience4j</groupId>
      <artifactId>resilience4j-bom</artifactId>
      <version>${resilience4j.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
<dependencies>
  <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
  <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>
  <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aspectj</artifactId></dependency>
  <dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot4</artifactId>
    <version>${resilience4j.version}</version>
  </dependency>
</dependencies>

Source: Resilience4j: Spring Boot 2 & 3 for the Boot 2 and 3 forms (there is no Boot 4 page on readme.io), the published POM of resilience4j-spring-boot4 and the Spring Boot 4.0 Migration Guide. Run: mvn dependency:tree resolved the Boot 2.7.18 pom (Resilience4j 2.3.0 modules, resilience4j-spring under the starter) and the Boot 3.5.16 pom (2.4.0, resilience4j-spring6); the Boot 4.1.1 pom is the build that compiled and ran every Bookshelf test on this page. Gradle users translate the same coordinates to implementation(…​) and mavenBom("io.github.resilience4j:resilience4j-bom:…​"), as shown in Getting Started.

What changes at each step

Step What happens Evidence

Boot 2 to Boot 3

Move to Resilience4j 2.0.1 or later with resilience4j-spring-boot3 (the Boot 2 starter does not run on Framework 6). The code changes are the Boot ones: Jakarta namespaces, and the Actuator and Micrometer upgrades. The Resilience4j keys are unchanged.

Release notes, 2.0.1

spring-boot-starter-aop to spring-boot-starter-aspectj

Spring Boot 4 retired the AOP starter; its last Central release is 4.0.0-M2. Without spring-boot-starter-aspectj the application starts, the registries and endpoints exist, and every @CircuitBreaker is silently a no-op.

Run, see the table of pitfalls

Version verifier

Each starter ships an auto-configuration that fails fast when the Boot major version is wrong. resilience4j-spring-boot3 2.4.0 on Boot 4.1.1 stops with "Module 'io.github.resilience4j:resilience4j-spring-boot3' is only compatible with Spring Boot 3.x"; the Boot 4 starter says 4.x.

Run on Boot 4.1.1

The BOM does not manage the Boot 4 starter

resilience4j-bom 2.4.0 lists spring-boot3 but neither resilience4j-spring-boot4, -hedge nor -commons-configuration. Give those an explicit version, as the pom above does.

mvn error "'dependencies.dependency.version' …​ is missing"

Actuator and web are not transitive

The Boot 4 starter declares its dependencies at runtime scope. Add spring-boot-starter-actuator and a web starter yourself.

Published POM

Modular HTTP client starters

With only spring-boot-starter-web or -webflux there is no RestClient.Builder or WebClient.Builder bean on Boot 4.1.1. Add spring-boot-starter-restclient and spring-boot-starter-webclient.

Run on Boot 4.1.1

HTTP client timeouts

The keys are spring.http.clients.connect-timeout and read-timeout. The singular spring.http.client.* of Boot 3 has no effect on Boot 4.1.1.

Run on Boot 4.1.1

Health package moves

HealthIndicator and Health moved from org.springframework.boot.actuate.health to org.springframework.boot.health.contributor, and StatusAggregator to org.springframework.boot.health.actuate.endpoint. Properties-only applications are unaffected.

Observability

Actuator security matcher

EndpointRequest moved to org.springframework.boot.security.autoconfigure.actuate.web.servlet

Observability

HTTP service clients

@ImportHttpServices, RestClientHttpServiceGroupConfigurer and spring.http.serviceclient.<group>.base-url replace hand-built proxy factories

Programmatic Usage

Refresh scope

resilience4j-spring-boot4 ships @RefreshScope registries; resilience4j-spring-boot3 does not

Spring Boot Configuration

Spring Cloud CircuitBreaker

5.0.3 still brings Resilience4j 2.3.0 and resilience4j-spring-boot3, which the verifier rejects on Boot 4 until you align it

Spring Cloud CircuitBreaker

Endpoint JSON

The stateTransition text of an event is State transition from CLOSED to OPEN on Boot 4.1.1 and CLOSED_TO_OPEN on Boot 3.5.16

Observability

Source: the runs recorded in the pages linked in the table, on Spring Boot 4.1.1 and 3.5.16, and the Spring Boot 4.0 Migration Guide.

A reasonable order for a Boot 2 service is: upgrade Resilience4j on Boot 2 only as far as 2.3.0, rename the removed keys, move to Boot 3 and resilience4j-spring-boot3, confirm the tests, and only then move to Boot 4.

Spring Retry to Spring Framework 7

Spring Retry (org.springframework.retry:spring-retry) is archived at spring-attic/spring-retry; the last release is 2.0.13 (8 June 2026). Spring Framework 7 replaced its core with @Retryable, @EnableResilientMethods, RetryTemplate and RetryPolicy. Spring Boot 3.5.16 still manages Spring Retry 2.0.13, Spring Boot 4.1.1 no longer does, and Framework 6.2 has no native API at all, so this migration belongs to the Boot 4 step. The complete attribute-by-attribute table, the behaviour of RetryTemplate, RetryListener and MethodRetryEvent, and the comparison with Resilience4j are in Spring Framework Native Resilience; this is the part you need to start.

Spring Retry Spring Framework 7 What to watch

@EnableRetry

@EnableResilientMethods

Without it, @Retryable is a silent no-op on Boot 4.1.1: nothing enables it automatically.

maxAttempts = 3 (total invocations)

maxRetries = 2 (retries after the first call)

The default of 3 is 3 invocations in Spring Retry and 4 in the Framework.

retryFor, noRetryFor

includes, excludes

Empty means every exception in both.

@Backoff(delay, multiplier, maxDelay)

delay, multiplier, maxDelay on the annotation

Default fixed one second in both; the multiplier of 1.0 is fixed.

@Backoff(random = true)

jitter

The Framework jitter is an absolute duration added to the delay.

@Recover

nothing

Catch the exception at the call site, or use a Resilience4j fallbackMethod. The Framework rethrows the original exception.

RetryTemplate.execute(callback, recoveryCallback)

RetryTemplate.invoke(…​) or execute(Retryable)

execute always throws RetryException on failure; invoke rethrows the original.

@CircuitBreaker, stateful = true

nothing in core

Resilience4j CircuitBreaker, or the Framework Retry implementation of Spring Cloud CircuitBreaker.

Source: Spring Retry (archived) and the Spring Framework reference: Resilience features. The full table was checked against the Spring Retry 2.0.13 and Spring Framework 7.0.9 sources, and the two annotations below were run again for this page.

The same retry before and after
// Spring Retry 2.0.13: three invocations in total, then @Recover answers
@Configuration
@EnableRetry
class OldConfig { }

class OldCovers {
    @org.springframework.retry.annotation.Retryable(retryFor = IOException.class, maxAttempts = 3,
                                                    backoff = @Backoff(delay = 10))
    String fetch(String isbn) throws IOException { return http.fetch(isbn); }

    @Recover
    String fallback(IOException e, String isbn) { return "no-cover"; }
}

// Spring Framework 7: maxRetries = maxAttempts - 1, the recovery moves to the caller
@Configuration
@EnableResilientMethods
class NewConfig { }

class NewCovers {
    @org.springframework.resilience.annotation.Retryable(includes = IOException.class, maxRetries = 2, delay = 10)
    String fetch(String isbn) throws IOException { return http.fetch(isbn); }
}

Source: Spring Framework reference: Resilience features and Spring Retry (archived). Run on Spring Boot 4.1.1 with @EnableRetry and @EnableResilientMethods in the same context: each processed its own annotation, the old bean was invoked 3 times and answered from @Recover with no-cover, and the new bean was invoked 3 times and threw the original IOException. Because both can be active together, the migration can be done one bean at a time.

When a retry is the only thing you use, the Framework annotation is enough and removes a dependency. When you also need a circuit breaker, back-off with jitter caps, a retry on a result, or metrics, move to the Resilience4j Retry instead, which is described in Retry; do not mix both on one method, because native @Retryable and Resilience4j @Retry multiply (3 by 3 gives 9 invocations).

What changed in 2.3.0 and 2.4.0

The 2.4.0 release notes are only in the GitHub release; the RELEASENOTES.adoc at the tag stops at 2.3.0, so the first table is from the file and the second from the release and the sources. The numbers are issue and pull request numbers.

2.3.0 change Effect Page

Custom Clock in CircuitBreakerConfig (issues 2234, 2239)

No internal API needed; a customizer can pass the application clock

Spring Boot Configuration

Lock-free sliding window (2241) and ReentrantLock instead of synchronized (2232)

No virtual-thread pinning in the core modules

Circuit Breaker

"Ignore unknown exceptions" in the circuit breaker configuration (2200) and the circular baseConfig fix (2190)

In the Boot 3 and 4 starters a misspelled exception class in recordExceptions still fails start-up, and ignoreClassBindingExceptions has no effect; a self-referencing baseConfig is reported as "Circular reference detected", while longer cycles still overflow the stack

Spring Boot Configuration

@RateLimiter(permits = n) (2121) and drainPermissionsOnResult for executeSuspendFunction (2179)

The annotation attribute is ignored by the Spring 6 aspect that both starters use

Rate Limiter

RxJava 3 in the Spring aspects (2131); Retry.decorateSupplier (2029)

RxJava 3 return types work on annotated methods

Reactive, Kotlin & RxJava

Exponential back-off or randomized wait from a base configuration (1404); randomize factor above 1.0 fix (2152)

Back-off inheritance, with gaps documented on the page

Retry

Feign 12.5 and later (2130)

The capability-based API is the supported one

Gateway & OpenFeign

2.4.0 change Effect Page

resilience4j-spring-boot4

The starter for Spring Boot 4 and Framework 7; not in the BOM

Getting Started

spring-boot2, spring-cloud2 and resilience4j-spring dropped

Boot 2 applications stay on 2.3.0

Spring Boot 2 to 3 to 4

Boot-version verifier in the Boot 3 and Boot 4 starters

The wrong starter fails at start-up instead of misbehaving

Spring Boot 2 to 3 to 4

initialState (PR 2268) and transitionToStateAfterWaitDuration (PR 2290)

New Boot keys on the circuit breaker

Circuit Breaker

ignoreExceptionsPrecedenceEnabled (PR 2304)

A builder flag only, not a Boot key; the state machine already let ignoreExceptions win

Circuit Breaker

TIME_BASED with LOCK_FREE and a window below 2 is rejected (PR 2358); constant of waitIntervalFunctionInOpenState fixed (PR 2402)

A configuration that used to be accepted can now fail

Circuit Breaker

configuration attribute on the annotations (PR 2329)

A shared configuration key for annotations whose name is a SpEL expression

Spring Boot Configuration

bulkheadAspectOrder setter (PR 2386)

The key binds from YAML; in 2.3.0 it was silently ignored

Annotations & Aspect Order

Customizers applied in the right order (PR 2321), Retry eventConsumerBufferSize inherited (PR 2344), SpEL regular expressions compiled once (PR 2398)

Configuration fixes in the Boot 3 and 4 starters

Spring Boot Configuration

@RefreshScope registries (PR 2384)

Boot 4 starter only

Spring Boot Configuration

TimeLimiter success path when a stage completes with null (PR 2374)

SUCCESS events for a null result went from 0 to 1

TimeLimiter

Bulkhead name in BulkheadFullException, withThreadPoolBulkhead on more Decorators builders, snapshot reads for event endpoints

Better messages and safer endpoints

Bulkhead

resilience4j-micrometer brings resilience4j-bulkhead transitively

With Spring Cloud CircuitBreaker, a CircuitBreaker fallback can see ExecutionException instead of the original exception

Spring Cloud CircuitBreaker

Resilience4jFeign.builder(FeignDecorator) deprecated

Use Resilience4jFeign.capability

Gateway & OpenFeign

Source: RELEASENOTES.adoc (2.3.0) and the v2.4.0 GitHub release with the v2.4.0 sources. Each row was run on the page it links; the 2.3.0 against 2.4.0 differences were measured by running the same code against both jars.

Where the readme.io documentation is stale

The Resilience4j user guide was last updated years before 2.4.0, and it has no page for Spring Boot 4 or for 2.4.0 at all. The table collects every disagreement found while writing this section. Where the page and the code differ, the section follows the v2.4.0 source and links the source next to the readme.io page. The last column is the page that explains the detail.

Setup, modules and examples

Topic readme.io says v2.4.0 behaviour Page

Spring Boot 4

Not covered: the setup page uses spring-boot-starter-aop, and there is no Boot 4 page or demo

resilience4j-spring-boot4 plus spring-boot-starter-aspectj; the starter is not in the BOM and the AspectJ starter is not transitive

Getting Started

Spring Cloud page

The legacy page documents resilience4j-spring-cloud2

The module was dropped in 2.4.0 (last release 2.3.0); use Spring Cloud CircuitBreaker, whose 5.0.3 still pins Resilience4j 2.3.0 and resilience4j-spring-boot3, so the versions must be aligned

Spring Cloud CircuitBreaker

Examples

Vavr Try and CheckedFunction1, jCenter(), Gradle compile

Vavr is gone from the core since 2.0.0, jCenter was shut down, and Gradle uses implementation; use Maven Central

Getting Started, Retry

Cache page

Written for 0.x: Vavr types and compile 'org.ehcache:ehcache:3.7.1'

io.github.resilience4j.core.functions.CheckedFunction; the module is in the BOM but not in the Boot starter

Cache & Hedge

Undocumented modules

No page for resilience4j-hedge, resilience4j-commons-configuration, or @Timer with the timers endpoint

All three exist; hedge and commons-configuration are not in the BOM, hedge is not in resilience4j-all, and hedge has no Boot properties

Cache & Hedge, Registries, Observability

Reactive libraries

reactor-core and rxjava are "not a transitive dependency"

The 2.4.0 POMs declare Reactor and both RxJava majors at compile scope, and the Boot-managed version wins

Reactive, Kotlin & RxJava

Unnamed breaker

CircuitBreaker.ofDefaults() without a name in the reactive examples

ofDefaults needs a name

Reactive, Kotlin & RxJava

Properties and behaviour of the patterns

Topic readme.io says v2.4.0 behaviour Page

Missing properties

The tables omit fairCallHandlingEnabled, drainPermissionsOnResult, slidingWindowSynchronizationStrategy, initialState, transitionToStateAfterWaitDuration, consumeResultBeforeRetryAttempt

Only initialState, transitionToStateAfterWaitDuration and consumeResultBeforeRetryAttempt are Boot keys. fairCallHandlingEnabled (builder: fairCallHandlingStrategyEnabled), drainPermissionsOnResult and slidingWindowSynchronizationStrategy are Java-only and are set with a customizer

Spring Boot Configuration

recordExceptions and ignoreExceptions

An ignored exception is neither a failure nor a success, even if it is also recorded: ignore wins

At run time ignore wins in 2.3.0 and 2.4.0, as the page says. The 2.4.0 flag ignoreExceptionsPrecedenceEnabled changes only what getRecordExceptionPredicate() returns ("legacy record-wins" in its Javadoc), and it is a builder flag, not a Boot property: setting it in YAML is silently ignored

Circuit Breaker

Other circuit breaker keys

No slidingWindowSynchronizationStrategy, clock or the 2.4.0 keys

slidingWindowSynchronizationStrategy and clock have no Boot key; TIME_BASED with LOCK_FREE ignores a configured clock

Circuit Breaker

Durations

waitDurationInOpenState: 10000

A bare number is milliseconds, so this is 10 seconds; prefer a suffix such as 10s

Spring Boot Configuration

Customizers

Six types, for instance names

Seven (TimerConfigCustomizer exists), and a customizer name can be a configuration name too; customizers run last and beat the command line

Spring Boot Configuration

Boot-only keys

Not listed: allowHealthIndicatorToFail, subscribeForEvents, ignoreClassBindingExceptions, recordResultPredicate, metrics.*, refresh behaviour

All are real; ignoreClassBindingExceptions binds but has no effect on Boot 3 and 4

Spring Boot Configuration

baseConfig

Not explained

A circuit breaker named baseConfig replaces configs.default; Retry, RateLimiter, TimeLimiter and Bulkhead always root at default; a self-reference fails with "Circular reference detected"

Spring Boot Configuration

Removed 1.x keys

Older pages and blog posts show ringBufferSizeInClosedState, maxRetryAttempts, contextPropagator

Removed in 2.0.1 and silently ignored

Resilience4j 1.x to 2.x

Retry

No consumeResultBeforeRetryAttempt

It exists in the builder and as a Boot key (a class name); the Boot back-off keys are silently ignored without a waitDuration

Retry

RateLimiter

No drainPermissionsOnResult; no wait behaviour stated

Java-only; @RateLimiter(permits = n) is ignored by the Spring 6 aspect; a rejected caller is parked for the whole timeoutDuration

Rate Limiter

Bulkhead

No contextPropagators or rejectedExecutionHandler; registry.bulkhead("name2", custom) uses a variable the page never defines

contextPropagators is a Boot key (class names); rejectedExecutionHandler is not a Boot key

Bulkhead

TimeLimiter

Programmatic API only; the blocking example executeFutureSupplier) → CompletableFuture.supplyAsync(() → helloWorldService::sayHelloWorld does not compile

Boot keys timeoutDuration (default one second) and cancelRunningFuture; @TimeLimiter supports only CompletionStage, Mono, Flux and RxJava types

TimeLimiter

Aspect order wording

"The higher value = the higher priority", with an example circuitBreakerAspectOrder: 1, retryAspectOrder: 2

Spring Ordered: a lower value is the outer aspect, so the example makes the breaker outer. Defaults: Retry (-5), CircuitBreaker (-4), RateLimiter (-3), TimeLimiter (-2), Bulkhead (-1), relative to LOWEST_PRECEDENCE

Annotations & Aspect Order, Fallbacks & Composition

Global fallback

A fallback method with only the exception parameter is "global"

It matches any annotated method of the same class (and its superclasses), not the application; resolution needs exact parameter types and an assignable return type, and ambiguity throws IllegalStateException per call

Fallbacks & Composition

Health, metrics and dashboards

Topic readme.io says v2.4.0 behaviour Page

Health indicator mapping

OPEN maps to DOWN and HALF_OPEN to UNKNOWN

OPEN maps to the custom status CIRCUIT_OPEN (DOWN only with allowHealthIndicatorToFail: true), HALF_OPEN to CIRCUIT_HALF_OPEN; the default aggregator leaves the top-level status UP; the indicators are off unless management.health.circuitbreakers.enabled is set

Observability

Micrometer metric names

The Micrometer page (written for 2.2.0) lists resilience4j.circuitbreaker.max.buffered.calls and a retry gauge with dotted kinds such as successful.without.retry

max.buffered.calls is not registered; the retry meter is a FunctionCounter named resilience4j_retry_calls_total with underscored kinds; Micrometer 1.17 adds seconds* suffixes for timers

Observability

Grafana dashboard

Old screenshots; the official JSON is the reference

The JSON queries the missing max_buffered_calls, needs the application tag, and covers only the circuit breaker, retry and the semaphore bulkhead

Observability

Kotlin TimeLimiter

A TimeoutCancellationException (rather than a TimeoutException)

The extension converts it and the runs show java.util.concurrent.TimeoutException

Reactive, Kotlin & RxJava

Other official pages that the section found stale

Topic Official page says Actual behaviour Page

RetryPolicy in the Spring Cloud CircuitBreaker reference

Lists withMaxDuration, withBackoff, forExceptions and combinators

Spring Framework 7.0.9 has withDefaults(), withMaxRetries(long) and builder(); none of those combinators

Spring Cloud CircuitBreaker

Gateway MVC reference

name: for the breaker in YAML

The source takes id

Gateway & OpenFeign

Release notes

RELEASENOTES.adoc is the changelog

The file at the v2.4.0 tag has no 2.4.0 section; the notes are only in the GitHub release

What changed in 2.3.0 and 2.4.0

Canonical URLs

github.com/spring-projects/spring-retry, projectreactor.io/docs, the Kubernetes liveness page, prometheus.io/docs/

They redirect or return 404 on 4 October 2026: use github.com/spring-attic/spring-retry, docs.spring.io/projectreactor/reactor-core/docs/current/reference/, kubernetes.io/docs/concepts/workloads/pods/probes/ and prometheus.io/docs/introduction/overview/

Reactive, Kotlin & RxJava

Migration pitfalls

Mistake What you see Fix

Migrating to Boot 4 with spring-boot-starter-aop

The application starts; @CircuitBreaker and @Retry do nothing

Use spring-boot-starter-aspectj

resilience4j-spring-boot3 on Boot 4

"Module 'io.github.resilience4j:resilience4j-spring-boot3' is only compatible with Spring Boot 3.x"

Use resilience4j-spring-boot4; with Spring Cloud CircuitBreaker 5.0.3, exclude the Boot 3 starter and import the BOM first

Importing the BOM and leaving the starter without a version

'dependencies.dependency.version' …​ is missing

Pin resilience4j-spring-boot4 (and -hedge) with ${resilience4j.version}

Keeping the 1.x keys

The breaker uses 100 calls and a window of 100; no error

Rename to slidingWindowSize, permittedNumberOfCallsInHalfOpenState, maxAttempts, contextPropagators

Copying Hystrix numbers without setting them

The breaker needs 100 calls and stays open for 60 seconds

Set minimumNumberOfCalls, waitDurationInOpenState and the window explicitly

@Retryable without @EnableResilientMethods on Boot 4

No retry, no error

Add the annotation, or use Resilience4j @Retry

maxAttempts = 3 copied to maxRetries = 3

Four invocations instead of three

maxRetries = maxAttempts - 1

Native @Retryable and Resilience4j @Retry on one method

Three times three, nine invocations

Use one retry layer per call

Trusting the readme.io aspect-order sentence

The breaker records one failure per retried call instead of three

Follow Ordered: lower is outer, and leave the defaults when you want Retry outside the breaker

Source: the runs on this page and on the pages linked in the tables above. More general anti-patterns are collected in Production Tuning & Anti-patterns.