Spring Boot Configuration

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.

Every Resilience4j instance in a Spring Boot service is described in application.yml, under a handful of resilience4j. prefixes: a default configuration, optional named configurations, and one entry per instance. This page is the reference for that file. It covers the complete structure, every property of every module with its default, how an instance’s effective configuration is resolved, the settings that exist only in Spring Boot, how XxxConfigCustomizer beans override the YAML, and how profiles, SpEL names and @RefreshScope interact with it. It closes with the complete *Bookshelf configuration. The semantics of each property are on the module pages (CircuitBreaker, Retry, RateLimiter, Bulkhead and TimeLimiter); the registry model behind the file is in Registries, Configuration & Decorators.

The page was written against Resilience4j 2.4.0 (released 14 March 2026), Spring Boot 4.1.1, Spring Framework 7.0.x and Java 21 and verified on 4 October 2026. Every YAML file and Java class below was bound and run in a sample project, and the same suite was repeated on Spring Boot 3.5.16 with resilience4j-spring-boot3 2.4.0. The property keys and their behaviour are the same on both lines. Two things differ and are called out where they matter: the Java package of the property classes (io.github.resilience4j.springboot on Boot 4, io.github.resilience4j.springboot3 on Boot 3), and the @RefreshScope registries, which only the Boot 4 starter ships.

Left: the shape that is repeated under every prefix, with tags, metrics switches, an aspect order key, configs with default and named entries, and instances with baseConfig and property overrides. Right: seven rows mapping the prefixes resilience4j.circuitbreaker, retry, ratelimiter, bulkhead, thread-pool-bulkhead, timelimiter and micrometer.timer to their registry beans, their aspect order keys and their XxxConfigCustomizer types. The thread-pool bulkhead has no aspect order key of its own and shares the bulkhead aspect.

How the starter reads the configuration

The starter registers one @ConfigurationProperties bean per prefix. Each is a thin class in resilience4j-spring-boot4 that extends a class in resilience4j-spring6 (which adds the aspect order) that extends a Common…​ConfigurationProperties class in resilience4j-framework-common. The common class holds everything interesting: the two maps configs and instances, the validation in the setters, the inheritance rules and the code that turns properties into a XxxConfig. That is why the Boot 3 and Boot 4 starters behave identically.

Table 1. The properties classes behind each prefix (Boot 4 starter; the Boot 3 starter has the same names in the springboot3 package)
Prefix Properties class Common base class (resilience4j-framework-common)

resilience4j.circuitbreaker

CircuitBreakerProperties

CommonCircuitBreakerConfigurationProperties

resilience4j.retry

RetryProperties

CommonRetryConfigurationProperties

resilience4j.ratelimiter

RateLimiterProperties

CommonRateLimiterConfigurationProperties

resilience4j.bulkhead

BulkheadProperties

CommonBulkheadConfigurationProperties

resilience4j.thread-pool-bulkhead

ThreadPoolBulkheadProperties

CommonThreadPoolBulkheadConfigurationProperties

resilience4j.timelimiter

TimeLimiterProperties

CommonTimeLimiterConfigurationProperties

resilience4j.micrometer.timer

TimerProperties

CommonTimerConfigurationProperties

resilience4j.scheduled.executor

ContextAwareScheduledThreadPoolProperties

ContextAwareScheduledThreadPoolConfigurationProperties

Source: resilience4j-framework-common at v2.4.0 and the resilience4j-spring-boot4 sources at v2.4.0. The prefixes were read from the @ConfigurationProperties annotations and from the spring-configuration-metadata.json inside the published resilience4j-spring-boot4-2.4.0.jar.

At startup the module’s XxxConfiguration in resilience4j-spring6 turns those properties into a registry, in this order:

  • every entry under configs is built into a XxxConfig and added to the registry as a named configuration; the entry called default becomes the registry’s default configuration;

  • every entry under instances is built into a XxxConfig and the instance is created in the registry under its name;

  • every XxxConfigCustomizer bean whose name is not already a configuration and not already created gets an instance of its own (see Customizers);

  • each instance is wired to an event consumer whose buffer holds eventConsumerBufferSize events (default 100).

An instance that appears in no YAML and no customizer is created later, on its first use by an annotation, from a named configuration or the default one. The diagram follows one instance through both paths.

flowchart TD D["Built-in defaults of the builder
e.g. 100-call window, 50 percent threshold"] --> B subgraph Y["application.yml, after Spring Boot has merged every property source"] B{"Does the instance name
a baseConfig?"} B -- "yes" --> C["configs.baseConfig
(circuit breaker: only this config;
other modules: configs.default first, then this one)"] B -- "no" --> E["configs.default
(when it exists)"] C --> I["instances.name keys
(only the keys that are present)"] E --> I end I --> Z["XxxConfigCustomizer bean
whose name() equals the instance name
(always applied last)"] Z --> R["Registry instance, created at startup"] A["Annotation on a method:
name and optional configuration"] --> Q{"Instance already
in the registry?"} Q -- "yes" --> U["Use it"] Q -- "no" --> K{"Registry has a configuration called
configuration, or else called name?"} K -- "yes" --> N["Create the instance from that configuration"] K -- "no" --> M["Create it from the registry default
(configs.default, else builder defaults)"]

Read the first chain as the startup path and the second as the runtime path. The YAML path is a pipeline of layers, each overriding only the keys it sets. The runtime path never reads instances; it asks the registry, which is why an annotation with a new name (or a SpEL-computed one) silently gets the default configuration and never an instances entry that you forgot to rename. Source: CircuitBreakerConfiguration (startup) and the CircuitBreakerAspect (runtime) at v2.4.0; the other modules follow the same pattern.

Where an instance starts from

The common classes differ in one important place, and the readme.io page does not say so. Each row below was confirmed by running a YAML file and reading the effective configuration back from the registry.

Table 2. What an instance inherits, per module
Module No baseConfig on the instance baseConfig names a configuration

CircuitBreaker

Starts from configs.default if it exists, otherwise from the builder defaults. An instance whose name is also the name of a configuration starts from that configuration.

Starts from that configuration only. configs.default is not applied to it, nor to the configuration it names (in the run below, minimumNumberOfCalls: 10, set only in default, did not reach pricing). A named configuration may itself name a baseConfig.

Retry, RateLimiter, Bulkhead, ThreadPoolBulkhead, TimeLimiter, Timer

Starts from configs.default if it exists, otherwise from the builder defaults.

Starts from that configuration, which is itself built on top of configs.default. The default is always the root of the chain (an instance with baseConfig: slow took limitForPeriod from default and limitRefreshPeriod from slow).

Source: CommonCircuitBreakerConfigurationProperties.createCircuitBreakerConfig and CommonRetryConfigurationProperties.createRetryConfig at v2.4.0. The behaviour for Retry, RateLimiter and TimeLimiter was run; Bulkhead, ThreadPoolBulkhead and Timer contain the same code (read, not run).

The next file makes the difference concrete. It has one default, one named configuration critical, and instances that use them in the three ways.

layers.yml: the same shape, resolved differently for each instance
resilience4j:
  circuitbreaker:
    configs:
      default:                                  # the base of every breaker that names no baseConfig
        slidingWindowSize: 20
        minimumNumberOfCalls: 10
        failureRateThreshold: 50
        waitDurationInOpenState: 30s
        registerHealthIndicator: true           # Boot only
      critical:                                 # a shared, named configuration
        slidingWindowSize: 10
        failureRateThreshold: 25
        waitDurationInOpenState: 10s
        enableExponentialBackoff: true
        exponentialBackoffMultiplier: 2
    instances:
      pricing:
        baseConfig: critical
        failureRateThreshold: 20                # wins over critical
      isbn:
        baseConfig: critical
        waitDurationInOpenState: 15s            # replaces the wait of critical, and drops its backoff
      reviews: {}                               # no baseConfig: starts from default
  retry:
    configs:
      default:
        maxAttempts: 3
        waitDuration: 200ms
        enableExponentialBackoff: true
        exponentialBackoffMultiplier: 2
      patient:
        maxAttempts: 5
    instances:
      isbn:
        baseConfig: patient                     # patient is built on top of default
        waitDuration: 1s

Source: Resilience4j: Spring Boot 2 & 3 (the configs/instances/baseConfig syntax) and the common classes above. Bound and run on Spring Boot 4.1.1 (JDK 21); the effective values read back from the registries are in the table.

Table 3. Effective configuration read back from the registries
Instance Window / minimum calls / failure rate Open-state waits (1st, 2nd, 3rd) Why

pricing

10 / 100 / 20

10 s, 20 s, 40 s

Starts from critical: its window, wait and exponential backoff apply, the instance’s failureRateThreshold: 20 wins. minimumNumberOfCalls: 10 from default is not applied, so the built-in 100 stays.

isbn

10 / 100 / 25

15 s, 15 s, 15 s

Overriding waitDurationInOpenState replaces the wait of critical and, because the CircuitBreaker’s backoff flags are not inherited, also drops its exponential backoff.

reviews, and any instance not in the file

20 / 10 / 50

30 s, 30 s, 30 s

No baseConfig: default applies.

Two further observations from the same run. First, the Boot-only property registerHealthIndicator: true that sits in default reached all four breakers, including pricing and isbn, which do not inherit default’s other keys: Boot-only properties are merged separately (see Boot-only properties). Second, the Retry module behaves the other way round on backoff. In the same file, the Retry instance `isbn that sets only waitDuration: 1s and baseConfig: patient waited 1 s, 2 s and 4 s and made five attempts: maxAttempts came from patient, and the exponential flags came from default, which Retry always merges in. The Retry merge carries enableExponentialBackoff, enableRandomizedWait, exponentialBackoffMultiplier and exponentialMaxWaitDuration; the CircuitBreaker merge carries none of them. The Retry merge has one gap: randomizedWaitFactor is not carried. With a default of waitDuration: 100ms, enableRandomizedWait: true and randomizedWaitFactor: 0.1, an instance that inherited all three drew waits between 90 ms and 110 ms, and an instance that overrode only waitDuration: 1000ms drew waits between 500 ms and 1500 ms, that is the built-in factor of 0.5.

A baseConfig that names no configuration fails the startup with ConfigurationNotFoundException: Configuration with name 'x' does not exist. An instance that names itself fails with IllegalStateException: Circular reference detected in instance config. A longer cycle, a configuration naming another that names the first, is not detected and ends in a StackOverflowError (reproduced for CircuitBreaker and Retry), so keep baseConfig chains short and acyclic.

The complete YAML structure

The file below spells out every key the 2.4.0 starter binds, under every prefix, at its default value. It is a reference to read, not a file to copy: the defaults are what you get without it. Keys that are Boot-only carry a comment. A few values are illustrative because they have no fixed default: the two thread-pool sizes (they depend on the number of CPUs), the class names, the team tag and the scheduled executor’s corePoolSize. The three reference-* instances show the keys that only make sense in combination (exponential backoff, randomized wait, predicate classes). Binding this exact file and reading the effective configuration back gave the same values as the ofDefaults() of CircuitBreakerConfig, RetryConfig, RateLimiterConfig, BulkheadConfig, TimeLimiterConfig and TimerConfig, and for the thread-pool bulkhead the same queue capacity and keep-alive.

application.yml: every key of every prefix, at its default
resilience4j:
  circuitbreaker:
    tags:                                       # added to every Micrometer meter of this module
      team: bookshelf
    circuitBreakerAspectOrder: 2147483643       # Ordered.LOWEST_PRECEDENCE - 4
    metrics:
      enabled: true
      legacy:
        enabled: false
    configs:
      default:
        registerHealthIndicator: false          # Boot only
        allowHealthIndicatorToFail: false       # Boot only
        eventConsumerBufferSize: 100            # Boot only
        slidingWindowType: COUNT_BASED
        slidingWindowSize: 100
        minimumNumberOfCalls: 100
        failureRateThreshold: 50
        slowCallRateThreshold: 100
        slowCallDurationThreshold: 60s
        waitDurationInOpenState: 60s
        permittedNumberOfCallsInHalfOpenState: 10
        maxWaitDurationInHalfOpenState: 0s
        transitionToStateAfterWaitDuration: OPEN
        automaticTransitionFromOpenToHalfOpenEnabled: false
        initialState: CLOSED
        writableStackTraceEnabled: true
        recordExceptions: []
        ignoreExceptions: []
    instances:
      reference-predicates:
        baseConfig: default
        recordFailurePredicate: com.example.bookshelf.PricingFailurePredicate
        recordResultPredicate: com.example.bookshelf.StalePriceIsAFailure
        ignoreExceptionPredicate: com.example.bookshelf.ClientErrorIsNotAFailure
        ignoreClassBindingExceptions: false
      reference-exponential:
        baseConfig: default
        waitDurationInOpenState: 20s
        enableExponentialBackoff: true
        exponentialBackoffMultiplier: 1.5
        exponentialMaxWaitDurationInOpenState: 10m
      reference-randomized:
        baseConfig: default
        waitDurationInOpenState: 20s
        enableRandomizedWait: true
        randomizedWaitFactor: 0.5
  retry:
    tags:
      team: bookshelf
    retryAspectOrder: 2147483642                # LOWEST_PRECEDENCE - 5
    metrics:
      enabled: true
      legacy:
        enabled: false
    configs:
      default:
        eventConsumerBufferSize: 100            # Boot only
        maxAttempts: 3
        waitDuration: 500ms
        failAfterMaxAttempts: false
        retryExceptions: []
        ignoreExceptions: []
    instances:
      reference-classes:
        baseConfig: default
        retryExceptionPredicate: com.example.bookshelf.TransientHttpFailure
        resultPredicate: com.example.bookshelf.StalePriceIsAFailure
        consumeResultBeforeRetryAttempt: com.example.bookshelf.LogRetriedResult
      reference-interval:
        baseConfig: default
        intervalBiFunction: com.example.bookshelf.RetryAfterIntervalFunction
      reference-backoff:
        baseConfig: default
        waitDuration: 500ms
        enableExponentialBackoff: true
        exponentialBackoffMultiplier: 1.5
        exponentialMaxWaitDuration: 30s
        enableRandomizedWait: true
        randomizedWaitFactor: 0.5
  ratelimiter:
    tags:
      team: bookshelf
    rateLimiterAspectOrder: 2147483644          # LOWEST_PRECEDENCE - 3
    metrics:
      enabled: true
      legacy:
        enabled: false
    configs:
      default:
        registerHealthIndicator: false          # Boot only
        allowHealthIndicatorToFail: false       # Boot only
        subscribeForEvents: false               # Boot only
        eventConsumerBufferSize: 100            # Boot only
        limitForPeriod: 50
        limitRefreshPeriod: 500ns
        timeoutDuration: 5s
        writableStackTraceEnabled: true
  bulkhead:
    tags:
      team: bookshelf
    bulkheadAspectOrder: 2147483646             # LOWEST_PRECEDENCE - 1
    metrics:
      enabled: true
      legacy:
        enabled: false
    configs:
      default:
        eventConsumerBufferSize: 100            # Boot only
        maxConcurrentCalls: 25
        maxWaitDuration: 0ms
        writableStackTraceEnabled: true
  thread-pool-bulkhead:
    tags:
      team: bookshelf
    metrics:
      enabled: true
      legacy:
        enabled: false
    configs:
      default:
        eventConsumerBufferSize: 100            # Boot only
        coreThreadPoolSize: 2                   # default: max(CPUs - 1, 1)
        maxThreadPoolSize: 4                    # default: number of CPUs
        queueCapacity: 100
        keepAliveDuration: 20ms
        writableStackTraceEnabled: true
        contextPropagators:
          - com.example.bookshelf.MdcContextPropagator
  timelimiter:
    tags:
      team: bookshelf
    timeLimiterAspectOrder: 2147483645          # LOWEST_PRECEDENCE - 2
    metrics:
      enabled: true
      legacy:
        enabled: false
    configs:
      default:
        eventConsumerBufferSize: 100            # Boot only
        timeoutDuration: 1s
        cancelRunningFuture: true
  micrometer:
    timer:
      tags:
        team: bookshelf
      timerAspectOrder: 2147483647              # Ordered.LOWEST_PRECEDENCE
      configs:
        default:
          eventConsumerBufferSize: 100          # Boot only
          metricNames: resilience4j.timer.calls
          onFailureTagResolver: com.example.bookshelf.CauseTag
  scheduled:
    executor:
      corePoolSize: 2
      contextPropagators:
        - com.example.bookshelf.MdcContextPropagator

Source: Resilience4j: Spring Boot 2 & 3 for the structure, and the Common…​ConfigurationProperties classes at v2.4.0 for the list of keys and their types. Bound and run on Spring Boot 4.1.1; the defaults were compared with CircuitBreakerConfig.ofDefaults(), RetryConfig.ofDefaults() and the other ofDefaults() factories. The file also bound on Spring Boot 3.5.16.

Three details of the structure are worth knowing before you write your own.

  • Property names are relaxed. slidingWindowSize, sliding-window-size and SLIDING_WINDOW_SIZE (as an environment variable) are the same key. The examples use camelCase because that is how the Resilience4j documentation writes the keys; the run also bound the kebab-case spelling given as a command-line argument over the camelCase one in the file.

  • Instance and configuration names are map keys. Keep them lower-case with hyphens (isbn-metadata) or camelCase (pricingFail) in YAML, but see environment variables for what happens to them there.

  • Legacy aliases still bind. resilience4j.circuitbreaker.backends, resilience4j.retry.backends, resilience4j.bulkhead.backends, resilience4j.thread-pool-bulkhead.backends and resilience4j.ratelimiter.limiters are the same map as instances (the getBackends() and getLimiters() getters return it). The TimeLimiter and Timer prefixes have no alias. Use instances in new files.

Names in environment variables

Environment variables are upper-cased and cannot contain hyphens, so they address the lower-case key. In a run with RESILIENCE4J_CIRCUITBREAKER_INSTANCES_PRICING_FAILURERATETHRESHOLD=55 the pricing instance took the value. An instance called pricingFail in the YAML, set through RESILIENCE4J_CIRCUITBREAKER_INSTANCES_PRICINGFAIL_SLIDINGWINDOWSIZE, took the value and an extra instance called pricingfail appeared in the registry. A variable for the hyphenated instance isbn-metadata (…​_ISBN_METADATA_SLIDINGWINDOWSIZE) was ignored. The practical rules are to give instances single-word lower-case names when they must be overridable from the environment, and to override long-lived values in the YAML or through a configuration server rather than through the process environment. Source: Spring Boot reference: Externalized Configuration.

Unknown keys are ignored

Spring Boot ignores keys it cannot bind, and Resilience4j adds no validation. A misspelled key (failureRateTreshold), a misspelled prefix (resilience4j.circuitbreakr) and a key that does not exist in the starter all start the application and leave the default in place. The run printed rate=50.0 for an instance configured with failureRateTreshold=10. Section Property reference lists the keys that exist; the next section lists the ones that people expect and that do not.

Settings that have no YAML key

Several builder settings have no Spring Boot property in 2.4.0, and setting them in YAML does nothing. Use a customizer (next section).

Table 4. Builder settings without a Boot key
Module Setting (builder method) Note

CircuitBreaker

slidingWindowSynchronizationStrategy, clock, ignoreExceptionsPrecedenceEnabled, waitIntervalFunctionInOpenState, currentTimestampFunction

ignoreExceptionsPrecedenceEnabled: true in YAML was accepted and had no effect (the effective config stayed false). The state machine already lets ignoreExceptions win; see the CircuitBreaker page.

Retry

intervalFunction, writableStackTraceEnabled, retryOnException with a lambda

The YAML keys waitDuration, enableExponentialBackoff and the others build an interval function for you; intervalBiFunction takes a class.

RateLimiter

drainPermissionsOnResult, the permits attribute of the annotation

@RateLimiter(permits = n) is not honoured by the Spring 6 aspect that both starters use; see the RateLimiter page.

Bulkhead

fairCallHandlingStrategyEnabled (the builder method; the field in BulkheadConfig is called fairCallHandlingEnabled)

Ignored in YAML.

ThreadPoolBulkhead

rejectedExecutionHandler, the thread factory

Ignored in YAML; the pool threads are platform threads.

TimeLimiter

none

Both keys, timeoutDuration and cancelRunningFuture, are properties.

Source: CommonCircuitBreakerConfigurationProperties.buildConfig and its siblings at v2.4.0: only the keys read there are applied. ignoreExceptionsPrecedenceEnabled and the ignoreClassBindingExceptions no-op were run on Spring Boot 4.1.1.

Durations

Every duration property is a java.time.Duration converted by Spring Boot, so the usual formats work, and the unit of a bare number is milliseconds. The table lists what was bound for resilience4j.retry.instances.x.waitDuration and read back from the interval function; the same converter serves every duration key.

Table 5. Duration formats accepted by the binder
Value in YAML Result Note

500ms, 5s, 2m, 1h, 1d

500 ms, 5 s, 2 min, 1 h, 1 day

The simple style: a number and one of ns, us, ms, s, m, h, d. Case does not matter (500MS worked).

PT0.5S, PT1M30S

500 ms, 90 s

The ISO-8601 style.

5000

5000 ms, that is 5 s

A bare number is milliseconds. The readme.io example waitDurationInOpenState: 10000 therefore means 10 seconds. Prefer a suffix.

0

0

Valid where zero makes sense: RateLimiter timeoutDuration, Bulkhead maxWaitDuration, maxWaitDurationInHalfOpenState, Retry waitDuration.

1.5s, 5 s

Startup failure: '1.5s' is not a valid duration

No fractions in the simple style (use 1500ms) and no spaces.

100us, 1500000ns

0 ms and 1 ms for Retry

Retry and the open-state waits keep milliseconds, so finer values are truncated. RateLimiter keeps nanoseconds: its default limitRefreshPeriod is 500 ns.

Source: Spring Boot reference: Converting Durations. Run on Spring Boot 4.1.1 and 3.5.16 with the values in the table.

Most setters also validate their input, and a bad value stops the application at startup with the setter’s message. The checks that were reached by running a bad value are in the table.

Table 6. Validation that stops the startup
Bad value Message (class IllegalArgumentException unless noted)

failureRateThreshold: 0 or 101; slowCallRateThreshold: 0

failureRateThreshold must be between 1 and 100., slowCallRateThreshold must be between 1 and 100.

slidingWindowSize: 0, minimumNumberOfCalls: 0, permittedNumberOfCallsInHalfOpenState: 0

slidingWindowSize must be greater than or equal to 1. (and the matching message)

transitionToStateAfterWaitDuration: HALF_OPEN

transitionToStateAfterWaitDuration must be either OPEN or CLOSED

slidingWindowType: SLIDING

No enum constant …​SlidingWindowType.SLIDING (enum values are case-insensitive: count_based binds)

enableExponentialBackoff and enableRandomizedWait both true on a circuit breaker with a waitDurationInOpenState

IllegalStateException: you can not enable Exponential backoff policy and randomized delay at the same time

maxAttempts: 0, waitDuration: -1s

maxAttempts must be greater than or equal to 1., Illegal argument waitDuration: PT-1S is negative

limitForPeriod: 0

Illegal argument limitForPeriod: 0 is less than 1

maxConcurrentCalls: 0

maxConcurrentCalls must be greater than or equal to 1.

timeoutDuration: -1s on a time limiter

…​ is negative (the message wrongly names exponentialMaxWaitDuration; ignore the name)

eventConsumerBufferSize: 0, on any module

eventConsumerBufferSize must be greater than or equal to 1.

maxAttempts: three

NumberFormatException: For input string: "three"

Source: the setters of the Common…​ConfigurationProperties.InstanceProperties classes at v2.4.0 (for example the circuit breaker one). The messages were produced by starting the application with the value on Spring Boot 4.1.1; the checks for minimumNumberOfCalls and permittedNumberOfCallsInHalfOpenState were read from the setters.

Exception, predicate and other class names

Seven kinds of property hold a class name. YAML gives them as fully qualified names, a list for the arrays, and Spring Boot’s Class conversion loads them at startup.

Table 7. Properties that take a class
Property Type the class must have Requirements

recordExceptions, ignoreExceptions (CircuitBreaker), retryExceptions, ignoreExceptions (Retry)

a list of Throwable subclasses

Fully qualified names. Outer$Inner and Outer.Inner both resolved for a nested class. The class must exist: a typo fails the startup with ClassNotFoundException. The type is not checked: java.lang.String was accepted, and simply never matches.

recordFailurePredicate, ignoreExceptionPredicate (CircuitBreaker), retryExceptionPredicate (Retry)

Predicate<Throwable>

A public class with a public no-argument constructor. It is created by reflection, not by Spring: it is not a bean, nothing is injected into it, and it is shared by every call.

recordResultPredicate (CircuitBreaker), resultPredicate (Retry)

Predicate<Object>

The same requirements.

consumeResultBeforeRetryAttempt (Retry)

BiConsumer<Integer, Object>

The same requirements. The attempt number comes first, then the result.

intervalBiFunction (Retry)

IntervalBiFunction<Object>

The same requirements. It overrides the backoff keys.

contextPropagators (ThreadPoolBulkhead, and resilience4j.scheduled.executor)

a list of ContextPropagator

A no-argument constructor. YAML entries and ThreadPoolBulkheadConfigCustomizer entries are added together.

onFailureTagResolver (Timer)

Function<Throwable, String>

The same requirements as a predicate.

Source: ClassUtils at v2.4.0 (getConstructor().newInstance()) and the InstanceProperties types. Each outcome in the table was produced on Spring Boot 4.1.1 and 3.5.16.

A wrong class fails fast, and with different messages: a class that does not exist gives ClassNotFoundException; a predicate without a public no-argument constructor gives NoSuchMethodException: …​<init>() (a package-private class is the same); a class that is not a Predicate gives ClassCastException. The ignoreClassBindingExceptions property does not soften the first of them in 2.4.0 (see Boot-only properties). Because a predicate is built from a class name, logic that needs a Spring bean, a feature flag or the request has to move to a customizer (next section) or into the decorated method itself.

A predicate named from YAML: public class, public no-argument constructor, no dependencies
package com.example.bookshelf;

import java.util.function.Predicate;

public class ClientErrorIsNotAFailure implements Predicate<Throwable> {
    @Override
    public boolean test(Throwable t) { return t instanceof IllegalArgumentException; }
}

Source: Resilience4j: Spring Boot 2 & 3 (the recordFailurePredicate style) and the ClassUtils source above. Compiled and bound through ignoreExceptionPredicate in the reference file on Spring Boot 4.1.1.

Lists behave as Spring Boot lists. A list in a higher-precedence source replaces the whole list of a lower one; it is not merged (see Profiles for a run). And an instance’s recordExceptions replaces, rather than extends, the list of its baseConfig: the builder is called with the instance’s array and the inherited record predicate is cleared.

Boot-only properties

A few properties do not belong to any XxxConfig. They configure what the starter builds around an instance: health indicators, event buffers, tags, metrics, aspect order. They live in the same instances and configs entries (the per-instance ones) or next to them (the per-prefix ones).

Table 8. Properties that exist only in Spring Boot
Property Where Default Effect

registerHealthIndicator

CircuitBreaker, RateLimiter instances

false

Include the instance in the circuitBreakers / rateLimiters health indicator. The indicator itself exists only when management.health.circuitbreakers.enabled / management.health.ratelimiters.enabled is true (default false).

allowHealthIndicatorToFail

CircuitBreaker, RateLimiter instances

false

Let an open breaker (or an exhausted limiter) turn the health DOWN. Otherwise the indicator reports its custom status, CIRCUIT_OPEN or RATE_LIMITED, which does not fail the health.

eventConsumerBufferSize

every module’s instances

100

Size of the ring buffer behind the *events Actuator endpoints. A value below 1 fails the startup. For the RateLimiter it applies only with subscribeForEvents.

subscribeForEvents

RateLimiter instances

false

Subscribe the event consumer so that the ratelimiterevents endpoint has data. The other modules always subscribe.

ignoreClassBindingExceptions

CircuitBreaker instances

none

Binds, but has no effect in the Spring Boot 3 and 4 starters: with true a misspelled class in recordExceptions still failed the startup with ClassNotFoundException. The converter that honoured it lives in the old resilience4j-spring module, which is not part of 2.4.0 (settings.gradle includes only spring6, spring-boot3 and spring-boot4).

tags

every prefix

empty

Tags added to every Micrometer meter of that module. resilience4j.circuitbreaker.tags.team=bookshelf produced a team tag next to name and state on resilience4j.circuitbreaker.state, and getTags() on the registry returned it.

metrics.enabled

every prefix except micrometer.timer

true

false stops the starter from binding that module’s meters (the run showed no resilience4j.circuitbreaker.* meters, only the rate limiter’s).

metrics.legacy.enabled

every prefix except micrometer.timer

false

Selects the Tagged…​Metrics binder bean instead of the Tagged…​MetricsPublisher. The meter names were identical in the run, so do not use it as a way to get different names.

circuitBreakerAspectOrder and the other *AspectOrder keys

per prefix

see the property reference

Position of the aspect. Covered on Annotations & Aspect Order.

Source: the InstanceProperties classes and ConfigUtils.mergePropertiesIfAny at v2.4.0, the additional-spring-configuration-metadata.json of the starter, and the health and metrics behaviour run on Spring Boot 4.1.1 and 3.5.16. The eventConsumerBufferSize default is the .orElse(100) in each module’s XxxConfiguration.

How these properties are inherited is different from the builder properties, and it is implemented in mergePropertiesIfAny. That method copies a Boot-only value into the instance when the instance does not set it, first from the baseConfig’s entry and then from `configs.default. It is the reason registerHealthIndicator: true in default reached every breaker in the layers.yml run, whatever its baseConfig. The method copies for CircuitBreaker registerHealthIndicator, allowHealthIndicatorToFail, eventConsumerBufferSize and ignoreClassBindingExceptions; for RateLimiter the same plus subscribeForEvents; for Retry eventConsumerBufferSize and the four backoff keys; and eventConsumerBufferSize only for Bulkhead, ThreadPoolBulkhead, TimeLimiter and Timer. The Retry eventConsumerBufferSize copy was added in 2.4.0 (PR 2344).

The IDE cannot help much with the per-instance keys. The published spring-configuration-metadata.json describes the maps (resilience4j.circuitbreaker.instances and .configs are listed as Map<String, InstanceProperties>), the per-prefix keys and the Actuator access and cache.time-to-live keys, but a map value type is not expanded into per-key completion, so slidingWindowSize under an instance is not offered. Use the tables on this page.

Customizers

YAML cannot express everything: a Clock, a lambda, a bean from the application context, or one of the settings in the table above. For those the starter looks for beans of the module’s customizer type and calls them with the Builder while it builds the configuration. There is one interface per module: CircuitBreakerConfigCustomizer, RetryConfigCustomizer, RateLimiterConfigCustomizer, BulkheadConfigCustomizer, ThreadPoolBulkheadConfigCustomizer, TimeLimiterConfigCustomizer and TimerConfigCustomizer. Each has a name() and a customize(Builder) method, and a static of(name, consumer) factory for the common case. The readme.io page lists six of the seven; the Timer one exists in 2.4.0.

A customizer that gives the pricing breaker the application’s clock
package com.example.bookshelf;

import io.github.resilience4j.common.circuitbreaker.configuration.CircuitBreakerConfigCustomizer;
import java.time.Clock;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
public class BookshelfResilienceConfig {

    @Bean
    @ConditionalOnMissingBean
    Clock clock() {
        return Clock.systemUTC();
    }

    @Bean
    CircuitBreakerConfigCustomizer pricingBreakerClock(Clock clock) {
        // the TIME_BASED window of "pricing" now reads the application's Clock bean,
        // which a test replaces with a fixed or manually advanced clock
        return CircuitBreakerConfigCustomizer.of("pricing", builder -> builder.clock(clock));
    }
}

Source: Resilience4j: Spring Boot 2 & 3 (the customizer example) and CircuitBreakerConfigCustomizer at v2.4.0. Compiled and run on Spring Boot 4.1.1 and 3.5.16: with a fixed Clock bean in a test, the breaker read back from the registry held that clock. Spring Boot defines no Clock bean, hence the @ConditionalOnMissingBean default.

The rules, each verified by a run:

  • The name selects what is customized. The name is an instance name or a configuration name. A customizer named default changes the default configuration and therefore every instance that inherits it; one named like a configs entry changes that configuration for the instances that use it. In a run, a customizer named default set minimumNumberOfCalls(8) and both an instance with baseConfig: default and an unlisted instance reported 8; a customizer for the configuration strict reached an instance with baseConfig: strict.

  • The customizer runs last and wins. It receives the builder after the YAML values were applied. With the YAML saying failureRateThreshold: 45, a command-line argument saying 60 and the customizer saying 20, the effective value was 20.

  • A customizer can create an instance. If its name is neither a configuration nor an instance already created, the starter creates an instance of that name at startup (a customizer for ghost made registry.find("ghost") non-empty before any call).

  • One customizer per name and module. Two beans for the same name fail the startup with IllegalStateException: It is not possible to define more than one customizer per instance name pricing. Put all the changes for a name in one bean.

  • Lists add up. ThreadPoolBulkheadConfigCustomizer appends to the contextPropagators from YAML; it does not replace them.

Source: CompositeCustomizer (one per name), the buildConfig methods (the customizer runs after the property keys), and initCircuitBreakerRegistry in CircuitBreakerConfiguration at v2.4.0. The 2.4.0 release includes the fix to the order in which customizers are applied in the Boot 3 starter (PR 2321, a port of an older Boot 2 fix).

The customizers below set what has no YAML key. They are compiled and applied in the same run: the circuit breaker read back LOCK_FREE, the retry read back waits of 100 ms and 200 ms, the bulkhead read back a non-fair strategy.

Customizers for settings that have no Boot property
package com.example.bookshelf;

import io.github.resilience4j.bulkhead.BulkheadConfig;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig.SlidingWindowSynchronizationStrategy;
import io.github.resilience4j.common.bulkhead.configuration.BulkheadConfigCustomizer;
import io.github.resilience4j.common.bulkhead.configuration.ThreadPoolBulkheadConfigCustomizer;
import io.github.resilience4j.common.circuitbreaker.configuration.CircuitBreakerConfigCustomizer;
import io.github.resilience4j.common.ratelimiter.configuration.RateLimiterConfigCustomizer;
import io.github.resilience4j.common.retry.configuration.RetryConfigCustomizer;
import io.github.resilience4j.core.IntervalFunction;
import java.time.Duration;
import java.util.concurrent.ThreadPoolExecutor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/** Settings that exist on the builders but have no Spring Boot property in 2.4.0. */
@Configuration(proxyBeanMethods = false)
public class ProgrammaticOnlyCustomizers {

    @Bean
    CircuitBreakerConfigCustomizer recommendationsBreaker() {
        return CircuitBreakerConfigCustomizer.of("recommendations", builder -> builder
            .slidingWindowSynchronizationStrategy(SlidingWindowSynchronizationStrategy.LOCK_FREE)
            .ignoreExceptionsPrecedenceEnabled(true));
    }

    @Bean
    RetryConfigCustomizer isbnRetry() {
        return RetryConfigCustomizer.of("isbn", builder -> builder
            .intervalFunction(IntervalFunction.ofExponentialBackoff(Duration.ofMillis(100), 2.0)));
    }

    @Bean
    RateLimiterConfigCustomizer searchLimiter() {
        return RateLimiterConfigCustomizer.of("search", builder -> builder
            .drainPermissionsOnResult(result -> result.isRight() && result.get() instanceof String s && s.isEmpty()));
    }

    @Bean
    BulkheadConfigCustomizer isbnBulkhead() {
        return BulkheadConfigCustomizer.of("isbn", builder -> builder.fairCallHandlingStrategyEnabled(false));
    }

    @Bean
    ThreadPoolBulkheadConfigCustomizer recommendationsPool() {
        return ThreadPoolBulkheadConfigCustomizer.of("recommendations", builder -> builder
            .rejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()));
    }
}

Source: CircuitBreakerConfig.Builder, RetryConfig.Builder, RateLimiterConfig.Builder and the bulkhead configs at v2.4.0. Compiled and run on Spring Boot 4.1.1. The thread-pool customizer shows the call; the default handler is already AbortPolicy, and CallerRunsPolicy would run the work on the request thread and defeat the isolation.

A customizer is an ordinary bean, so a profile can switch it (@Profile("test")) and it can take collaborators as constructor or method arguments, which a class named in YAML cannot. The registry is built from the customizers, so a customizer cannot inject the registry: the run failed with BeanCurrentlyInCreationException (an unresolvable circular reference).

Precedence

Two separate orderings apply, and it helps not to mix them. The first is Spring Boot’s: which property source provides the value of a key. The second is Resilience4j’s: which layer of a configuration provides a setting once the keys are known.

Which property source wins

Spring Boot merges every source into one environment before the starter reads it, so the usual order applies, from weakest to strongest: the packaged application.yml, the packaged profile files, application.yml files outside the jar and their profile files, OS environment variables, JVM system properties, SPRING_APPLICATION_JSON and command-line arguments. A value imported with spring.config.import (a Spring Cloud Config Server, a mounted file, a config tree) takes the position of the file that imports it, with its own profile variants after it. The runs confirmed that a command-line argument beats the YAML (--…​failure-rate-threshold=60 over failureRateThreshold: 45, even in a different spelling), that a system property beats the YAML, and that an environment variable beats the YAML. Source: Spring Boot reference: Externalized Configuration, order.

Which layer wins

Table 9. Layers of one instance’s configuration, weakest first
Layer Source Notes

1

The builder defaults of the module (XxxConfig.custom())

For example a 100-call window and a 5 s limiter timeout. The table in Property reference lists them.

2

configs.default

Applies to every instance of the module that does not name a baseConfig (and, except for the CircuitBreaker, as the root of every baseConfig chain).

3

configs.<baseConfig>, following its own baseConfig chain

Replaces configs.default for the CircuitBreaker.

4

instances.<name>

Only the keys that are present.

5

The XxxConfigCustomizer named after the instance, applied after each level

The customizer for default runs when the default is built, the one for a baseConfig when that configuration is built, and the one for the instance last. A command-line value does not beat it.

6

@CircuitBreaker(configuration = "x") and the other annotations' configuration attribute

Not a layer of the YAML: it chooses which registry configuration a not-yet-existing instance is created from. It never changes an existing instance.

Source: the buildConfig and createXxxConfig methods of the Common…​ConfigurationProperties classes at v2.4.0, the six aspects (configKey = annotation.configuration().isEmpty() ? name : annotation.configuration()), and the runs described in this page. Registering a configuration for an instance that already exists does nothing: registry.circuitBreaker(name, config) returns the existing instance and ignores the new configuration, as shown on the Registries, Configuration & Decorators page.

Profiles

Profiles are a Spring Boot feature, and Resilience4j needs nothing special: a profile-specific file or a profile-activated document is one more property source, merged before the starter reads the properties. The general mechanics are on the Configuration & Profiles page; what follows is the Resilience4j side of it. The practical use is to keep the production values in application.yml and to override only what a test or a laptop needs: tiny windows and waits so a test can open a breaker with four calls (see Testing Resilience), or breakers that never open while you debug.

The multi-document form keeps the profile in the same file. This file was run with and without the test profile: the wait was 200 ms, then 1 ms.

One file, two documents: the second applies only with the test profile
resilience4j:
  retry:
    configs:
      default:
        maxAttempts: 3
        waitDuration: 200ms
---
spring:
  config:
    activate:
      on-profile: test
resilience4j:
  retry:
    configs:
      default:
        waitDuration: 1ms

Source: Spring Boot reference: Profiles and Multi-document files. Run on Spring Boot 4.1.1 with --spring.profiles.active=test.

Four things go wrong in practice, and each one was reproduced.

  • An instance value beats a profile’s default. A test profile that shortens configs.default.waitDurationInOpenState does not reach an instance that sets its own wait. In the Bookshelf run, isbn kept its jittered 10 s wait under a profile that had set default to 100 ms, until the profile also overrode the instance. Override at the same level the production file sets the value.

  • A list replaces the whole list. A profile that sets recordExceptions for pricing to one class made the breaker stop recording the two classes of the base file (the run showed IOException recorded without the profile and not recorded with it). Repeat every element you want to keep.

  • The access or exposure keys travel with the profile too. Keep management.endpoint.circuitbreakers.access: read-only in the base file so that no profile can quietly re-enable the write operation.

  • Do not branch on the profile in Java. A customizer that checks environment.acceptsProfiles("test") hides the difference from the YAML. Make the difference a property.

The two profile files of the Bookshelf configuration are in the last section.

SpEL instance names

The name of every Resilience4j annotation can be computed. The aspect passes the string to a SpelResolver before it asks the registry, and the resolver recognizes three shapes; anything else is used literally. The same resolver also handles the fallbackMethod attribute, so a fallback name can be a placeholder too.

Table 10. Name expressions, as run
Expression in name Resolved to Rule

${bookshelf.resilience.pricing-instance:pricing}

pricing-v2 (the value in the Environment)

A whole-string placeholder (also #{…​}) is resolved through the embedded value resolver, which supports ${key:default}. The key does not have to be in the Resilience4j prefix.

#tenant.tier, #isbn, #p0, #root.args[0]

gold, the ISBN, the first argument, the first argument

A string that starts with # is evaluated as SpEL against the method arguments. Parameter names need the -parameters compiler flag (Spring Boot’s Maven parent and Gradle plugin set it); #p0 and #root.args[0] do not.

#root.methodName, #root.className

search, com.example.bookshelf.SpelNames

#root exposes methodName, className and args.

@instanceNames.forIsbn(#isbn)

isbn-978

A string that starts with @ is a SpEL expression that can call a bean, with the method arguments available.

'literal-' + #isbn

'literal-' + #isbn, literally

An expression must start with # or @ to be evaluated. This one became the instance name, quotes and all.

#isbn.length()

ClassCastException on every call

The expression must return a String.

Source: DefaultSpelResolver at v2.4.0 (the three regular expressions) and Spring Framework reference: Expression Language. Every row was run on Spring Boot 4.1.1 (the placeholder, #tenant.tier, #root.methodName and bean forms with the class below, the other forms with a test class of the same shape); the table shows what appeared in the registry.

Annotations that compute their instance name
package com.example.bookshelf;

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
import org.springframework.stereotype.Component;

@Component
public class SpelNames {

    public record Tenant(String id, String tier) {}

    // placeholder, resolved from the Environment: the instance name lives in application.yml
    @CircuitBreaker(name = "${bookshelf.resilience.pricing-instance:pricing}")
    public String price(String isbn) {
        return "9.99";
    }

    // a method argument, by parameter name: one breaker per tenant tier, all created
    // from the "tier" configuration instead of from configs.default
    @CircuitBreaker(name = "#tenant.tier", configuration = "tier")
    public String catalogue(Tenant tenant) {
        return "catalogue";
    }

    // the method name: the limiter is called "search", like the instance in application.yml
    @RateLimiter(name = "#root.methodName")
    public String search(String query) {
        return "results";
    }

    // a bean method that receives the argument; the bean is called instanceNames
    @CircuitBreaker(name = "@instanceNames.forIsbn(#isbn)", configuration = "tier")
    public String metadata(String isbn) {
        return "metadata";
    }
}

Source: Resilience4j: Spring Boot 2 & 3 (SpEL in annotation names) and the resolver above. In the run, the calls created the instances pricing-v2, gold, silver and isbn-978, and the limiter search. The bean called instanceNames is a component with one method, forIsbn(String isbn), that returns "isbn-" plus the first three characters.

The consequence of a computed name is on the configuration side. The registry knows no configuration called gold, so the instance is created from configs.default, which is almost never what you meant. The configuration attribute fixes that: @CircuitBreaker(name = "#tenant.tier", configuration = "tier") created gold and silver from the tier configuration (window 7 and rate 30 in the run) instead of the default (window 20 and rate 50). The attribute was added in 2.4.0 under the release note "Provide key for components using SpEL for names" (PR 2329). Two warnings follow. First, a computed name creates an instance per distinct value, and each has its own state, window and Micrometer series, so never compute a name from unbounded input such as an ID or a URL. Second, the instance is not in instances, so it has no entry of its own: its registerHealthIndicator and eventConsumerBufferSize come from configs.default.

@RefreshScope registries

With Spring Cloud’s RefreshScope available, the Boot 4 starter makes the six registries refresh-scoped: changing the properties and refreshing the context rebuilds them from the new values. This works for the CircuitBreaker, Retry, RateLimiter, Bulkhead, ThreadPoolBulkhead and TimeLimiter registries; the Timer registry and the aspects are not refresh-scoped.

sequenceDiagram participant Src as Config source (Git, Config Server, file) participant Ep as POST /actuator/refresh participant CR as ContextRefresher participant Reg as CircuitBreakerRegistry (refresh-scoped proxy) participant Asp as CircuitBreakerAspect Src->>Ep: value changed, operator or webhook calls refresh Ep->>CR: refresh() CR->>CR: reload the environment, rebind the properties beans CR-->>Ep: names of the changed keys CR->>Reg: destroy the scoped target Note over Reg: every instance, its state and its window are dropped Asp->>Reg: next annotated call Reg->>Reg: rebuild from the rebound properties, customizers applied Reg-->>Asp: a new CircuitBreaker, CLOSED, with the new configuration

The sequence is what a run on Spring Boot 4.1.1 showed. The scoped target was scopedTarget.circuitBreakerRegistry (and the five siblings), the bean seen by the application was a proxy, and an annotated call after the refresh went through a new breaker.

Maven: Spring Cloud’s context module, which provides RefreshScope and the refresh endpoint
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-context</artifactId>
  <version>5.0.3</version>
</dependency>

Source: Spring Cloud Commons: Refresh Scope and the CircuitBreakerRefreshScopedRegistryAutoConfiguration at v2.4.0 (active when RefreshScope is on the classpath and a RefreshScope bean exists). Version 5.0.3 is the Spring Cloud Commons release managed by the Spring Cloud 2025.1.3 BOM and was the one run with Spring Boot 4.1.1. If you also use Spring Cloud CircuitBreaker, import resilience4j-bom before spring-cloud-dependencies as described there.

To see the whole loop, the application below imported an external file with spring.config.import, exposed the refresh endpoint and had the file edited:

Refreshing the circuit breaker thresholds through the Actuator endpoint
# application.yml points at the file that the platform mounts or a config server serves
#   spring.config.import: optional:file:/etc/bookshelf/resilience.yml
#   management.endpoints.web.exposure.include: refresh,circuitbreakers

# after the file changed
curl -s -X POST http://localhost:8080/actuator/refresh
# ["resilience4j.circuitbreaker.instances.pricing.failureRateThreshold"]

curl -s http://localhost:8080/actuator/circuitbreakers
# {"circuitBreakers":{"pricing":{"failureRateThreshold":"20.0%","state":"CLOSED", ...}}}

Source: Spring Cloud Commons: Refresh Scope. The responses are from the run: the refresh returned the changed key, and /actuator/circuitbreakers then showed the new threshold of 20 where the file had said 50.

The behaviour to plan for:

  • State is lost. The refreshed registry is a new object. A breaker that was OPEN is CLOSED again, with an empty window (the run went from OPEN to CLOSED), and a limiter or bulkhead starts full. Refreshing during an incident re-closes a breaker that was protecting a failing dependency.

  • Held references go stale. An instance fetched earlier (CircuitBreaker cb = registry.circuitBreaker("pricing") stored in a field) is detached: in the run the old instance stayed OPEN and nothing called through it any more. Fetch instances from the registry for each use, or use the annotations, which do.

  • Metrics follow the new registry. After the refresh the resilience4j.circuitbreaker.state gauge for pricing reported the new instance (open back to 0) and the series count was unchanged.

  • The endpoint is a write operation. POST /actuator/refresh changes the running configuration. Do not expose it on the public port; protect it like the circuitbreakers write operation (a management port, or Spring Security rules on /actuator/**).

  • Prefer a rolling restart for incident-time changes. Refresh is useful for tuning thresholds in a calm system; it is a poor tool for changes that must not reset state.

Spring Boot 3 differs. The resilience4j-spring-boot3 2.4.0 jar contains no refresh-scoped registry auto-configuration. On Spring Boot 3.5.16 with spring-cloud-context 4.3.0, POST /actuator/refresh reported the changed key and rebound the properties bean, but the registry was a plain InMemoryCircuitBreakerRegistry, still held the old instance and still reported the old threshold. If you need it on Boot 3, define the registry bean yourself: it backs the auto-configuration off, and this is the same wiring the Boot 4 starter ships. With this bean the Boot 3 run behaved like the Boot 4 one (rate changed from 50 to 20, state CLOSED).

Spring Boot 3: a refresh-scoped CircuitBreakerRegistry of your own
package com.example.bookshelf;

import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.circuitbreaker.event.CircuitBreakerEvent;
import io.github.resilience4j.common.CompositeCustomizer;
import io.github.resilience4j.common.circuitbreaker.configuration.CircuitBreakerConfigCustomizer;
import io.github.resilience4j.consumer.EventConsumerRegistry;
import io.github.resilience4j.core.registry.RegistryEventConsumer;
import io.github.resilience4j.spring6.circuitbreaker.configure.CircuitBreakerConfiguration;
import io.github.resilience4j.spring6.circuitbreaker.configure.CircuitBreakerConfigurationProperties;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/** Spring Boot 3 starter 2.4.0 has no refresh-scoped registry: this is the same wiring the Boot 4 starter ships. */
@Configuration(proxyBeanMethods = false)
public class RefreshableCircuitBreakers {

    @Bean
    @RefreshScope
    CircuitBreakerRegistry circuitBreakerRegistry(
            CircuitBreakerConfigurationProperties properties,
            EventConsumerRegistry<CircuitBreakerEvent> eventConsumerRegistry,
            RegistryEventConsumer<CircuitBreaker> registryEventConsumer,
            @Qualifier("compositeCircuitBreakerCustomizer")
            CompositeCustomizer<CircuitBreakerConfigCustomizer> customizers) {
        return new CircuitBreakerConfiguration(properties)
                .circuitBreakerRegistry(eventConsumerRegistry, registryEventConsumer, customizers);
    }
}

Source: the Boot 4 auto-configuration it copies. Compiled and run on Spring Boot 3.5.16 with spring-cloud-context 4.3.0. Repeat the pattern per registry if you need it for Retry or the others; the Boot 3 starter has compositeXxxCustomizer beans with the same qualifier names.

Property reference

Every property below was read from the InstanceProperties classes and the Config defaults at v2.4.0, and its default was confirmed by binding the reference file and comparing with ofDefaults(). "Default" is the value an instance gets when neither the YAML nor a customizer sets the key and no configs.default provides one. All of these keys go under configs.<name> or instances.<name> of the module’s prefix, plus baseConfig (a string naming a configs entry, with no default). The meaning of each key is on the module’s own page.

Table 11. resilience4j.circuitbreaker: instance and configuration keys
Key Type Default Notes

slidingWindowType

COUNT_BASED, TIME_BASED

COUNT_BASED

Case-insensitive.

slidingWindowSize

int

100

At least 1. Calls for COUNT_BASED, seconds for TIME_BASED.

minimumNumberOfCalls

int

100

At least 1.

failureRateThreshold

float

50

Percent, 1 to 100.

slowCallRateThreshold

float

100

Percent, 1 to 100. The default of 100 means slow calls never open the breaker on their own.

slowCallDurationThreshold

Duration

60s

Greater than zero.

waitDurationInOpenState

Duration

60s

At least 1 ms. The base of the exponential and randomized variants below.

enableExponentialBackoff

boolean

false

Needs waitDurationInOpenState in the same source. Not combinable with enableRandomizedWait.

exponentialBackoffMultiplier

double

1.5 when the backoff is on

Greater than zero. Ignored without a wait and a flag.

exponentialMaxWaitDurationInOpenState

Duration

no cap

At least 1 ms.

enableRandomizedWait

boolean

false

Needs waitDurationInOpenState.

randomizedWaitFactor

double

0.5 when the randomization is on

0 up to, not including, 1.

permittedNumberOfCallsInHalfOpenState

int

10

At least 1.

maxWaitDurationInHalfOpenState

Duration

0 (wait for the probes without limit)

At least 0.

transitionToStateAfterWaitDuration

OPEN, CLOSED

OPEN

HALF_OPEN is rejected. New in 2.4.0.

automaticTransitionFromOpenToHalfOpenEnabled

boolean

false

A scheduler moves OPEN to HALF_OPEN; otherwise the first call after the wait does.

initialState

CircuitBreaker.State

CLOSED

New in 2.4.0.

writableStackTraceEnabled

boolean

true

false makes CallNotPermittedException cheaper.

recordExceptions, ignoreExceptions

list of classes

empty (every exception is recorded, none ignored)

See class names. An instance’s list replaces the inherited one.

recordFailurePredicate, ignoreExceptionPredicate

class, Predicate<Throwable>

none

recordResultPredicate

class, Predicate<Object>

none

A result for which it returns true counts as a failure.

registerHealthIndicator, allowHealthIndicatorToFail, eventConsumerBufferSize, ignoreClassBindingExceptions

boolean, boolean, int, boolean

false, false, 100, no effect

Boot only, see Boot-only properties.

Table 12. resilience4j.retry: instance and configuration keys
Key Type Default Notes

maxAttempts

int

3

At least 1, and it counts the first call.

waitDuration

Duration

500ms

At least 0. The base of the backoff variants below.

enableExponentialBackoff

boolean

false

Needs waitDuration in the same source (or inherited through the merge).

exponentialBackoffMultiplier

double

1.5 when the backoff is on

Greater than zero.

exponentialMaxWaitDuration

Duration

no cap

At least 0. Applied after the jitter.

enableRandomizedWait

boolean

false

May be combined with enableExponentialBackoff, unlike on the circuit breaker.

randomizedWaitFactor

double

0.5 when the randomization is on

0 up to, not including, 1.

retryExceptions, ignoreExceptions

list of classes

empty (every exception is retried)

ignoreExceptions wins over retryExceptions.

retryExceptionPredicate

class, Predicate<Throwable>

none

resultPredicate

class, Predicate<Object>

none

A result for which it returns true is retried.

consumeResultBeforeRetryAttempt

class, BiConsumer<Integer, Object>

none

intervalBiFunction

class, IntervalBiFunction<Object>

none

Overrides the backoff keys.

failAfterMaxAttempts

boolean

false

Only affects resultPredicate retries: throw MaxRetriesExceededException when the result is still unacceptable.

eventConsumerBufferSize

int

100

Boot only.

Table 13. resilience4j.ratelimiter: instance and configuration keys
Key Type Default Notes

limitForPeriod

int

50

At least 1.

limitRefreshPeriod

Duration

500ns

timeoutDuration

Duration

5s

How long a caller waits for a permit. 0 rejects at once.

writableStackTraceEnabled

boolean

true

registerHealthIndicator, allowHealthIndicatorToFail, subscribeForEvents, eventConsumerBufferSize

boolean, boolean, boolean, int

false, false, false, 100

Boot only.

Table 14. resilience4j.bulkhead (semaphore): instance and configuration keys
Key Type Default Notes

maxConcurrentCalls

int

25

At least 1.

maxWaitDuration

Duration

0

At least 0. 0 rejects at once.

writableStackTraceEnabled

boolean

true

eventConsumerBufferSize

int

100

Boot only.

Table 15. resilience4j.thread-pool-bulkhead: instance and configuration keys
Key Type Default Notes

coreThreadPoolSize

int

number of CPUs minus one, at least 1

A value of 0 or less means "not set".

maxThreadPoolSize

int

number of CPUs

A value of 0 or less means "not set".

queueCapacity

int

100

0 creates a SynchronousQueue; a negative value means "not set".

keepAliveDuration

Duration

20ms

writableStackTraceEnabled

boolean

true

contextPropagators

list of classes

empty

Added to those from a customizer.

eventConsumerBufferSize

int

100

Boot only.

Table 16. resilience4j.timelimiter: instance and configuration keys
Key Type Default Notes

timeoutDuration

Duration

1s

Not negative.

cancelRunningFuture

boolean

true

Ignored by the CompletionStage form.

eventConsumerBufferSize

int

100

Boot only.

Table 17. resilience4j.micrometer.timer and resilience4j.scheduled.executor
Key Type Default Notes

micrometer.timer metricNames

String

resilience4j.timer.calls

Name of the timer meter.

micrometer.timer onFailureTagResolver

class, Function<Throwable, String>

the exception’s simple class name

Value of the exception tag on failures.

micrometer.timer eventConsumerBufferSize

int

100

Boot only.

scheduled.executor.corePoolSize

int

none: no bean unless set

At least 1. Creates a ContextAwareScheduledThreadPoolExecutor bean, which the Retry and TimeLimiter aspects then use for their asynchronous scheduling (otherwise each creates its own pool with one thread per CPU).

scheduled.executor.contextPropagators

list of classes

empty

Propagate MDC or a security context onto those scheduler threads.

Table 18. Keys directly under each prefix, and the Actuator and management keys
Prefix Aspect order key (default) Meters and tags Related management keys

resilience4j.circuitbreaker

circuitBreakerAspectOrder (2147483643, that is Ordered.LOWEST_PRECEDENCE - 4)

tags, metrics.enabled (true), metrics.legacy.enabled (false)

management.health.circuitbreakers.enabled (false)

resilience4j.retry

retryAspectOrder (2147483642, LOWEST_PRECEDENCE - 5)

the same three

none

resilience4j.ratelimiter

rateLimiterAspectOrder (2147483644, LOWEST_PRECEDENCE - 3)

the same three

management.health.ratelimiters.enabled (false)

resilience4j.bulkhead

bulkheadAspectOrder (2147483646, LOWEST_PRECEDENCE - 1; bindable from YAML since 2.4.0)

the same three

none

resilience4j.thread-pool-bulkhead

none (the bulkhead aspect handles both types)

the same three

none

resilience4j.timelimiter

timeLimiterAspectOrder (2147483645, LOWEST_PRECEDENCE - 2)

the same three

none

resilience4j.micrometer.timer

timerAspectOrder (2147483647, Ordered.LOWEST_PRECEDENCE)

tags

none

all endpoints

management.endpoint.<id>.access (unrestricted), management.endpoint.<id>.cache.time-to-live (0ms), for the ids circuitbreakers, circuitbreakerevents, retries, retryevents, ratelimiters, ratelimiterevents, bulkheads, bulkheadevents, timelimiters, timelimiterevents, timers and timerevents

Source: Common…​ConfigurationProperties for the keys, validation and Boot-only properties; the CircuitBreakerConfig, RetryConfig, RateLimiterConfig, BulkheadConfig, ThreadPoolBulkheadConfig, TimeLimiterConfig and TimerConfig for the defaults; spring-configuration-metadata.json for the management and metrics keys. The thread-pool defaults were read back on an 8-CPU machine as core 7 and maximum 8. The readme.io Spring Boot page lists the common keys but omits allowHealthIndicatorToFail, subscribeForEvents, ignoreClassBindingExceptions, initialState, transitionToStateAfterWaitDuration, recordResultPredicate, the metrics.* switches and the refresh behaviour.

The property classes expose a total of 28 circuit breaker keys, 16 retry keys, 9 rate limiter keys, 5 bulkhead keys, 8 thread-pool bulkhead keys, 4 time limiter keys and 4 timer keys, counting baseConfig and the Boot-only ones. The tables above list every one of them: the counts were taken by reflecting over the setters of the 2.4.0 classes.

The Bookshelf configuration

The Bookshelf services call four things: the pricing service (critical, high traffic), the third-party ISBN-metadata API (rate limited, occasionally flaky), the recommendations service (optional, slow) and the reviews service (optional, tight latency budget); its own search endpoint needs protecting. The file below configures all of it, using every mechanism of this page: a shared default per module, instances that name it, overrides where an instance differs, Boot-only keys, class names for the predicate and the propagator, and the management keys. It was bound and run, and the effective values in the table are what the registries reported.

application.yml of bookshelf-service: the complete resilience configuration
spring:
  application:
    name: bookshelf-service

resilience4j:
  circuitbreaker:
    tags:
      application: ${spring.application.name}
    configs:
      default:                                  # what an unlisted breaker gets
        slidingWindowType: COUNT_BASED
        slidingWindowSize: 20
        minimumNumberOfCalls: 10
        failureRateThreshold: 50
        waitDurationInOpenState: 30s
        permittedNumberOfCallsInHalfOpenState: 3
        automaticTransitionFromOpenToHalfOpenEnabled: true
        registerHealthIndicator: true
        eventConsumerBufferSize: 50
        recordExceptions:
          - org.springframework.web.client.ResourceAccessException
          - org.springframework.web.client.HttpServerErrorException
          - java.util.concurrent.TimeoutException
        ignoreExceptions:
          - org.springframework.web.client.HttpClientErrorException
    instances:
      pricing:                                  # critical, high traffic: time-based window
        baseConfig: default
        slidingWindowType: TIME_BASED
        slidingWindowSize: 10
        minimumNumberOfCalls: 20
        slowCallDurationThreshold: 800ms
        slowCallRateThreshold: 70
        waitDurationInOpenState: 20s
        enableExponentialBackoff: true
        exponentialBackoffMultiplier: 2
        exponentialMaxWaitDurationInOpenState: 2m
        permittedNumberOfCallsInHalfOpenState: 5
        maxWaitDurationInHalfOpenState: 10s
        allowHealthIndicatorToFail: false
      isbn:                                     # third-party API: jitter the probes
        baseConfig: default
        waitDurationInOpenState: 10s
        enableRandomizedWait: true
        randomizedWaitFactor: 0.25
        registerHealthIndicator: false
      recommendations:                          # optional feature: open early, tolerate slowness
        baseConfig: default
        failureRateThreshold: 30
        slowCallDurationThreshold: 1s
      reviews:
        baseConfig: default

  retry:
    configs:
      default:
        maxAttempts: 3
        waitDuration: 200ms
        enableExponentialBackoff: true
        exponentialBackoffMultiplier: 2
        enableRandomizedWait: true
        randomizedWaitFactor: 0.5
        ignoreExceptions:
          - io.github.resilience4j.circuitbreaker.CallNotPermittedException
    instances:
      pricing:
        baseConfig: default
        maxAttempts: 2
        retryExceptions:
          - org.springframework.web.client.ResourceAccessException
      isbn:
        baseConfig: default
        maxAttempts: 4
        exponentialMaxWaitDuration: 5s
        retryExceptionPredicate: com.example.bookshelf.TransientHttpFailure
        eventConsumerBufferSize: 200

  ratelimiter:
    configs:
      default:
        limitForPeriod: 10
        limitRefreshPeriod: 1s
        timeoutDuration: 0
    instances:
      isbn:                                     # client side: stay under the third-party quota
        baseConfig: default
        limitForPeriod: 5
        timeoutDuration: 500ms
        subscribeForEvents: true
        registerHealthIndicator: true
      search:                                   # server side: protect our own endpoint
        baseConfig: default
        limitForPeriod: 3
        limitRefreshPeriod: 2s
        subscribeForEvents: true

  bulkhead:
    configs:
      default:
        maxConcurrentCalls: 10
        maxWaitDuration: 0
    instances:
      isbn:
        baseConfig: default
        maxConcurrentCalls: 2
        maxWaitDuration: 100ms
        writableStackTraceEnabled: false

  thread-pool-bulkhead:
    configs:
      default:
        coreThreadPoolSize: 2
        maxThreadPoolSize: 4
        queueCapacity: 2
        keepAliveDuration: 20ms
        contextPropagators:
          - com.example.bookshelf.MdcContextPropagator
    instances:
      recommendations:
        baseConfig: default

  timelimiter:
    configs:
      default:
        timeoutDuration: 2s
        cancelRunningFuture: true
    instances:
      pricing:
        baseConfig: default
        timeoutDuration: 800ms
      recommendations:
        baseConfig: default
        timeoutDuration: 1s
      reviews:
        baseConfig: default
        timeoutDuration: 300ms
        cancelRunningFuture: false

management:
  endpoints:
    web:
      exposure:
        include: health,circuitbreakers,circuitbreakerevents,retries,ratelimiters,bulkheads,timelimiters,metrics,prometheus
  endpoint:
    circuitbreakers:
      access: read-only                         # no CLOSE / FORCE_OPEN / DISABLE over HTTP
    health:
      show-details: when-authorized
  health:
    circuitbreakers:
      enabled: true
    ratelimiters:
      enabled: true

Source: Resilience4j: Spring Boot 2 & 3 and the property classes at v2.4.0. Bound and run on Spring Boot 4.1.1 and 3.5.16 (the test harness read it as bookshelf.yml). The classes named in it are com.example.bookshelf.TransientHttpFailure (see the Retry page) and com.example.bookshelf.MdcContextPropagator (see the Bulkhead page); ResourceAccessException, HttpServerErrorException and HttpClientErrorException are Spring’s org.springframework.web.client types, and CallNotPermittedException is Resilience4j’s own.

Table 19. Effective configuration read back from the registries (production profile)
Instance Result Remark

breaker pricing

TIME_BASED, 10 s window, 20 calls minimum, 50 percent, slow calls 800 ms at 70 percent, open waits 20 s, 40 s, 80 s, 5 probes, own Clock

The Clock comes from the customizer below.

breaker isbn

COUNT_BASED, 20 calls, 10 calls minimum, open wait randomized around 10 s (between 7.5 s and 12.5 s)

No health indicator.

breaker recommendations

20 calls, 30 percent, slow calls 1 s

breaker reviews, and any other name

20 calls, 10 minimum, 50 percent, 30 s wait

The default configuration.

retry pricing, isbn, other

2 attempts, 4 attempts, 3 attempts; waits jittered around 200 ms doubling

The isbn cap of 5 s and the predicate come from the instance.

limiter isbn, search, other

5 per second with 500 ms patience, 3 per 2 s, 10 per second

default rejects at once (timeoutDuration: 0).

bulkhead isbn

2 concurrent calls, 100 ms wait, no stack trace

thread-pool bulkhead recommendations

2 core threads, 4 maximum, queue of 2, MDC propagator

time limiters pricing, recommendations, reviews, other

800 ms, 1 s, 300 ms without cancelling, 2 s

Actuator

circuitbreakers exposes GET only

access: read-only removed the POST circuitbreakers/{name} operation; with unrestricted both were present.

Source: the run described above, on Spring Boot 4.1.1 (and with the same values on 3.5.16). The effective values were read with registry.circuitBreaker(name).getCircuitBreakerConfig() and the equivalents; the Actuator operations from the WebEndpointsSupplier bean of a servlet application.

The test profile shortens everything a test would otherwise wait for. Its isbn block exists only because of the pitfall described under Profiles.

application-test.yml: tiny windows and waits, no jitter
# Test profile: small windows, tiny waits and no jitter, so a test can open a breaker with four calls
resilience4j:
  circuitbreaker:
    configs:
      default:
        slidingWindowSize: 4
        minimumNumberOfCalls: 4
        waitDurationInOpenState: 100ms
        permittedNumberOfCallsInHalfOpenState: 1
    instances:
      pricing:
        slidingWindowType: COUNT_BASED
        slidingWindowSize: 4
        minimumNumberOfCalls: 4
        enableExponentialBackoff: false
        waitDurationInOpenState: 100ms
      isbn:                                     # the instance sets its own wait, so the default above would not reach it
        waitDurationInOpenState: 100ms
        enableRandomizedWait: false
  retry:
    configs:
      default:
        waitDuration: 1ms
        enableExponentialBackoff: false
        enableRandomizedWait: false
  ratelimiter:
    instances:
      isbn:
        limitForPeriod: 2
  timelimiter:
    configs:
      default:
        timeoutDuration: 500ms

Source: Spring Boot reference: Profiles. Run with spring.profiles.active=test: the breaker read back a window of 4 and a wait of 100 ms for pricing, isbn and unlisted names, the retries waited 1 ms, and the time-limiter default was 500 ms while pricing kept its 800 ms.

application-local.yml: breakers that stay closed while debugging
# Local profile: breakers that never trip while you debug, everything else as in production
resilience4j:
  circuitbreaker:
    configs:
      default:
        failureRateThreshold: 100
        minimumNumberOfCalls: 1000
  retry:
    configs:
      default:
        maxAttempts: 1

Source: Spring Boot reference: Profiles. Run with spring.profiles.active=local: isbn read back a minimum of 1000 calls and a threshold of 100 percent, and recommendations a minimum of 1000 calls but its own 30 percent. pricing took the 100 percent threshold from default but kept its own minimum of 20 calls, and the retries pricing (2 attempts) and isbn (4 attempts) kept their own maxAttempts, so the profile’s single attempt reached only instances that do not set the key. A profile that changes default does not reach an instance that sets the same key.

The customizer from the Customizers section completes the configuration; the application’s Clock is what a test replaces:

A test that replaces the Clock and reads the breaker back
package com.example.bookshelf;

import static org.assertj.core.api.Assertions.assertThat;

import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;

@SpringBootTest(classes = PricingBreakerClockTest.TestApp.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
@ActiveProfiles("test")
class PricingBreakerClockTest {

    @SpringBootConfiguration
    @EnableAutoConfiguration
    @Import(BookshelfResilienceConfig.class)
    static class TestApp {
    }

    @TestConfiguration
    static class FixedClock {
        @Bean
        Clock clock() {
            return Clock.fixed(Instant.parse("2026-10-04T12:00:00Z"), ZoneOffset.UTC);
        }
    }

    @Autowired
    CircuitBreakerRegistry registry;

    @Autowired
    Clock clock;

    @Test
    void pricingBreakerReadsTheApplicationClock() {
        assertThat(registry.circuitBreaker("pricing").getCircuitBreakerConfig().getClock()).isSameAs(clock);
        assertThat(registry.circuitBreaker("pricing").getCircuitBreakerConfig().getSlidingWindowSize()).isEqualTo(4);
    }
}

Source: Resilience4j: Spring Boot 2 & 3 (customizers). The test passed on Spring Boot 4.1.1 with the three YAML files above as application.yml, application-test.yml and application-local.yml: the fixed clock replaced the default one, the pricing breaker’s configuration held that very instance, and the test profile gave it a window of 4. Driving the breaker’s time-based window with a manual clock is the subject of Testing Resilience.

Pitfalls

Table 20. Configuration pitfalls, all reproduced on 2.4.0
Pitfall What happens and what to do

A misspelled key or prefix

It is ignored and the default applies, silently. Check the effective configuration through the registry (or /actuator/circuitbreakers) after a change, and use the tables above.

Setting a key that has no Boot property (ignoreExceptionsPrecedenceEnabled, fairCallHandlingEnabled, rejectedExecutionHandler, slidingWindowSynchronizationStrategy, drainPermissionsOnResult)

Accepted and ignored. Use a customizer.

Relying on ignoreClassBindingExceptions

It has no effect in the Boot 3 and Boot 4 starters. A class name that does not exist always fails the startup.

Assuming baseConfig stacks on configs.default

For the circuit breaker it replaces it: a key that only default sets does not reach an instance with a baseConfig. For the other modules default is always the root. Repeat the key in the named configuration, or avoid baseConfig on breakers.

Overriding only the wait of an instance that inherits an exponential breaker

The circuit breaker drops the exponential backoff (its flags are not inherited). The retry keeps the flags, but loses an inherited randomizedWaitFactor and falls back to 0.5. Repeat the flags and the factor on the instance.

A profile that changes configs.default while instances set the same key

The instance value wins. Override at the same level, or leave the key out of the instance.

A profile that changes a list

The list is replaced as a whole. Repeat the elements you keep.

Overriding an instance with an environment variable

The key is lower-cased: pricingFail gains a second instance pricingfail, and a hyphenated name cannot be addressed. Use lower-case single-word names, or do not override through the environment.

recordExceptions with a typo, a predicate class with constructor arguments, a class that is not a Predicate

The application stops at startup (ClassNotFoundException, NoSuchMethodException, ClassCastException). A class that is not a Throwable is accepted and never matches.

Expecting injection into a predicate, interval function or propagator class

They are created by reflection, not by Spring. Use a customizer bean when a collaborator is needed.

Two customizers for the same name, or a customizer that injects the registry

Startup failure (IllegalStateException, BeanCurrentlyInCreationException). One customizer per name; no registry dependency.

Expecting a command-line argument to override a customizer

The customizer runs last and wins. Take the value from @Value or a properties bean inside the customizer if it must be overridable.

A bare number as a duration

It is milliseconds: waitDurationInOpenState: 10000 is 10 s and limitRefreshPeriod: 1 is 1 ms. Write the unit.

A baseConfig cycle

Only a direct self-reference is reported; a longer cycle ends in a StackOverflowError.

A SpEL name that does not start with # or @, or does not return a String

The text is used literally, or every call throws ClassCastException. Computed names also bypass instances: add configuration = "…​", and keep the set of names small.

registerHealthIndicator: true and nothing in /actuator/health

The indicator needs management.health.circuitbreakers.enabled (or ratelimiters) set to true; the default is false.

@RefreshScope on Spring Boot 3

The Boot 3 starter does not make the registries refreshable; the properties rebind but the registry keeps its values. Define a refresh-scoped registry yourself, as shown above, or restart.

Refreshing during an incident

The new registry starts CLOSED with empty windows. Do not refresh while a breaker is open, and protect POST /actuator/refresh.

Exposing the circuitbreakers endpoint with its write operation

One unauthenticated POST can force a breaker open or disable it. Set management.endpoint.circuitbreakers.access: read-only in the base file and keep management on a protected port.

Source: the runs and source references of the sections above, on Spring Boot 4.1.1 and 3.5.16 with Resilience4j 2.4.0.

What changed recently

  • Boot 2 to Boot 3 to Boot 4. The resilience4j.* keys are the same on all three. What changed is the starter (resilience4j-spring-boot3, then resilience4j-spring-boot4, which is not in the BOM), the AspectJ starter, and the Actuator health package; resilience4j-spring-boot2 and resilience4j-spring-cloud2 stopped at 2.3.0. See Getting Started and Migration & What’s Changed.

  • 2.3.0. The ignoreClassBindingExceptions setter appeared in the properties class (comparing the 2.2.0 and 2.3.0 jars), and the circular-reference StackOverflowError in baseConfig handling was fixed for the self-reference case only.

  • 2.4.0, new keys. initialState and transitionToStateAfterWaitDuration on the circuit breaker (PRs 2268 and 2290), and the configuration attribute on the annotations, for SpEL names (PR 2329). None of these exist in the 2.3.0 properties classes or annotations (confirmed by comparing the jars).

  • 2.4.0, fixes that touch configuration. bulkheadAspectOrder became bindable from YAML, because the setter was added (PR 2386); in 2.3.0 the key was silently ignored. The Retry eventConsumerBufferSize is now inherited from the base configuration (PR 2344). Customizers are applied in the right order in the Boot 3 starter (PR 2321). SpEL regular expressions are compiled once (PR 2398).

  • 2.4.0, the Boot 4 starter. resilience4j-spring-boot4 ships the @RefreshScope registry auto-configurations (PR 2384). The Boot 3 starter does not.

  • Endpoint access. management.endpoint.<id>.access (none, read-only, unrestricted) works on both Spring Boot lines that were run, 3.5.16 and 4.1.1, and read-only removes the circuit breaker write operation.

  • The documentation. The readme.io Spring Boot page predates all of this. It shows a duration as bare milliseconds (waitDurationInOpenState: 10000), lists six customizer types (not TimerConfigCustomizer), says customizers are for instance names (they also accept configuration names), and does not mention the Boot-only keys listed above, the aspect order keys, initialState, the precedence rules or @RefreshScope. The 2.4.0 notes are only in the GitHub release: RELEASENOTES.adoc stops at 2.3.0.