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.
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.
| Prefix | Properties class | Common base class (resilience4j-framework-common) |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
configsis built into aXxxConfigand added to the registry as a named configuration; the entry calleddefaultbecomes the registry’s default configuration; -
every entry under
instancesis built into aXxxConfigand the instance is created in the registry under its name; -
every
XxxConfigCustomizerbean 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
eventConsumerBufferSizeevents (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.
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.
| Module | No baseConfig on the instance |
baseConfig names a configuration |
|---|---|---|
CircuitBreaker |
Starts from |
Starts from that configuration only. |
Retry, RateLimiter, Bulkhead, ThreadPoolBulkhead, TimeLimiter, Timer |
Starts from |
Starts from that configuration, which is itself built on top of |
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.
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.
| Instance | Window / minimum calls / failure rate | Open-state waits (1st, 2nd, 3rd) | Why |
|---|---|---|---|
|
10 / 100 / 20 |
10 s, 20 s, 40 s |
Starts from |
|
10 / 100 / 25 |
15 s, 15 s, 15 s |
Overriding |
|
20 / 10 / 50 |
30 s, 30 s, 30 s |
No |
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.
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-sizeandSLIDING_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.backendsandresilience4j.ratelimiter.limitersare the same map asinstances(thegetBackends()andgetLimiters()getters return it). The TimeLimiter and Timer prefixes have no alias. Useinstancesin 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).
| Module | Setting (builder method) | Note |
|---|---|---|
CircuitBreaker |
|
|
Retry |
|
The YAML keys |
RateLimiter |
|
|
Bulkhead |
|
Ignored in YAML. |
ThreadPoolBulkhead |
|
Ignored in YAML; the pool threads are platform threads. |
TimeLimiter |
none |
Both keys, |
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.
| Value in YAML | Result | Note |
|---|---|---|
|
500 ms, 5 s, 2 min, 1 h, 1 day |
The simple style: a number and one of |
|
500 ms, 90 s |
The ISO-8601 style. |
|
5000 ms, that is 5 s |
A bare number is milliseconds. The readme.io example |
|
0 |
Valid where zero makes sense: RateLimiter |
|
Startup failure: |
No fractions in the simple style (use |
|
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 |
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.
| Bad value | Message (class IllegalArgumentException unless noted) |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.
| Property | Type the class must have | Requirements |
|---|---|---|
|
a list of |
Fully qualified names. |
|
|
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. |
|
|
The same requirements. |
|
|
The same requirements. The attempt number comes first, then the result. |
|
|
The same requirements. It overrides the backoff keys. |
|
a list of |
A no-argument constructor. YAML entries and |
|
|
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.
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).
| Property | Where | Default | Effect |
|---|---|---|---|
|
CircuitBreaker, RateLimiter instances |
|
Include the instance in the |
|
CircuitBreaker, RateLimiter instances |
|
Let an open breaker (or an exhausted limiter) turn the health |
|
every module’s instances |
|
Size of the ring buffer behind the |
|
RateLimiter instances |
|
Subscribe the event consumer so that the |
|
CircuitBreaker instances |
none |
Binds, but has no effect in the Spring Boot 3 and 4 starters: with |
|
every prefix |
empty |
Tags added to every Micrometer meter of that module. |
|
every prefix except |
|
|
|
every prefix except |
|
Selects the |
|
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.
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
defaultchanges the default configuration and therefore every instance that inherits it; one named like aconfigsentry changes that configuration for the instances that use it. In a run, a customizer nameddefaultsetminimumNumberOfCalls(8)and both an instance withbaseConfig: defaultand an unlisted instance reported 8; a customizer for the configurationstrictreached an instance withbaseConfig: 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
ghostmaderegistry.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.
ThreadPoolBulkheadConfigCustomizerappends to thecontextPropagatorsfrom 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.
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
| Layer | Source | Notes |
|---|---|---|
1 |
The builder defaults of the module ( |
For example a 100-call window and a 5 s limiter timeout. The table in Property reference lists them. |
2 |
|
Applies to every instance of the module that does not name a |
3 |
|
Replaces |
4 |
|
Only the keys that are present. |
5 |
The |
The customizer for |
6 |
|
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.
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 shortensconfigs.default.waitDurationInOpenStatedoes not reach an instance that sets its own wait. In the Bookshelf run,isbnkept its jittered 10 s wait under a profile that had setdefaultto 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
recordExceptionsforpricingto one class made the breaker stop recording the two classes of the base file (the run showedIOExceptionrecorded without the profile and not recorded with it). Repeat every element you want to keep. -
The
accessor exposure keys travel with the profile too. Keepmanagement.endpoint.circuitbreakers.access: read-onlyin 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.
Expression in name |
Resolved to | Rule |
|---|---|---|
|
|
A whole-string placeholder (also |
|
|
A string that starts with |
|
|
|
|
|
A string that starts with |
|
|
An expression must start with |
|
|
The expression must return a |
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.
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.
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.
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:
# 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
OPENisCLOSEDagain, with an empty window (the run went fromOPENtoCLOSED), 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 stayedOPENand 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.stategauge forpricingreported the new instance (openback to 0) and the series count was unchanged. -
The endpoint is a write operation.
POST /actuator/refreshchanges the running configuration. Do not expose it on the public port; protect it like thecircuitbreakerswrite 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).
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.
| Key | Type | Default | Notes |
|---|---|---|---|
|
|
|
Case-insensitive. |
|
int |
|
At least 1. Calls for |
|
int |
|
At least 1. |
|
float |
|
Percent, 1 to 100. |
|
float |
|
Percent, 1 to 100. The default of 100 means slow calls never open the breaker on their own. |
|
Duration |
|
Greater than zero. |
|
Duration |
|
At least 1 ms. The base of the exponential and randomized variants below. |
|
boolean |
|
Needs |
|
double |
|
Greater than zero. Ignored without a wait and a flag. |
|
Duration |
no cap |
At least 1 ms. |
|
boolean |
|
Needs |
|
double |
|
0 up to, not including, 1. |
|
int |
|
At least 1. |
|
Duration |
|
At least 0. |
|
|
|
|
|
boolean |
|
A scheduler moves |
|
|
|
New in 2.4.0. |
|
boolean |
|
|
|
list of classes |
empty (every exception is recorded, none ignored) |
See class names. An instance’s list replaces the inherited one. |
|
class, |
none |
|
|
class, |
none |
A result for which it returns |
|
boolean, boolean, int, boolean |
|
Boot only, see Boot-only properties. |
| Key | Type | Default | Notes |
|---|---|---|---|
|
int |
|
At least 1, and it counts the first call. |
|
Duration |
|
At least 0. The base of the backoff variants below. |
|
boolean |
|
Needs |
|
double |
|
Greater than zero. |
|
Duration |
no cap |
At least 0. Applied after the jitter. |
|
boolean |
|
May be combined with |
|
double |
|
0 up to, not including, 1. |
|
list of classes |
empty (every exception is retried) |
|
|
class, |
none |
|
|
class, |
none |
A result for which it returns |
|
class, |
none |
|
|
class, |
none |
Overrides the backoff keys. |
|
boolean |
|
Only affects |
|
int |
|
Boot only. |
| Key | Type | Default | Notes |
|---|---|---|---|
|
int |
|
At least 1. |
|
Duration |
|
|
|
Duration |
|
How long a caller waits for a permit. |
|
boolean |
|
|
|
boolean, boolean, boolean, int |
|
Boot only. |
| Key | Type | Default | Notes |
|---|---|---|---|
|
int |
|
At least 1. |
|
Duration |
|
At least 0. |
|
boolean |
|
|
|
int |
|
Boot only. |
| Key | Type | Default | Notes |
|---|---|---|---|
|
int |
number of CPUs minus one, at least 1 |
A value of 0 or less means "not set". |
|
int |
number of CPUs |
A value of 0 or less means "not set". |
|
int |
|
|
|
Duration |
|
|
|
boolean |
|
|
|
list of classes |
empty |
Added to those from a customizer. |
|
int |
|
Boot only. |
| Key | Type | Default | Notes |
|---|---|---|---|
|
Duration |
|
Not negative. |
|
boolean |
|
Ignored by the |
|
int |
|
Boot only. |
| Key | Type | Default | Notes |
|---|---|---|---|
|
String |
|
Name of the timer meter. |
|
class, |
the exception’s simple class name |
Value of the |
|
int |
|
Boot only. |
|
int |
none: no bean unless set |
At least 1. Creates a |
|
list of classes |
empty |
Propagate MDC or a security context onto those scheduler threads. |
| Prefix | Aspect order key (default) | Meters and tags | Related management keys |
|---|---|---|---|
|
|
|
|
|
|
the same three |
none |
|
|
the same three |
|
|
|
the same three |
none |
|
none (the bulkhead aspect handles both types) |
the same three |
none |
|
|
the same three |
none |
|
|
|
none |
all endpoints |
|
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.
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.
| Instance | Result | Remark |
|---|---|---|
breaker |
|
The |
breaker |
|
No health indicator. |
breaker |
20 calls, 30 percent, slow calls 1 s |
|
breaker |
20 calls, 10 minimum, 50 percent, 30 s wait |
The |
retry |
2 attempts, 4 attempts, 3 attempts; waits jittered around 200 ms doubling |
The |
limiter |
5 per second with 500 ms patience, 3 per 2 s, 10 per second |
|
bulkhead |
2 concurrent calls, 100 ms wait, no stack trace |
|
thread-pool bulkhead |
2 core threads, 4 maximum, queue of 2, MDC propagator |
|
time limiters |
800 ms, 1 s, 300 ms without cancelling, 2 s |
|
Actuator |
|
|
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.
# 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.
# 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:
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
| 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 |
Setting a key that has no Boot property ( |
Accepted and ignored. Use a customizer. |
Relying on |
It has no effect in the Boot 3 and Boot 4 starters. A class name that does not exist always fails the startup. |
Assuming |
For the circuit breaker it replaces it: a key that only |
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 |
A profile that changes |
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: |
|
The application stops at startup ( |
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 ( |
Expecting a command-line argument to override a customizer |
The customizer runs last and wins. Take the value from |
A bare number as a duration |
It is milliseconds: |
A |
Only a direct self-reference is reported; a longer cycle ends in a |
A SpEL name that does not start with |
The text is used literally, or every call throws |
|
The indicator needs |
|
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 |
Exposing the |
One unauthenticated |
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, thenresilience4j-spring-boot4, which is not in the BOM), the AspectJ starter, and the Actuator health package;resilience4j-spring-boot2andresilience4j-spring-cloud2stopped at 2.3.0. See Getting Started and Migration & What’s Changed. -
2.3.0. The
ignoreClassBindingExceptionssetter appeared in the properties class (comparing the 2.2.0 and 2.3.0 jars), and the circular-referenceStackOverflowErrorinbaseConfighandling was fixed for the self-reference case only. -
2.4.0, new keys.
initialStateandtransitionToStateAfterWaitDurationon the circuit breaker (PRs 2268 and 2290), and theconfigurationattribute 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.
bulkheadAspectOrderbecame bindable from YAML, because the setter was added (PR 2386); in 2.3.0 the key was silently ignored. The RetryeventConsumerBufferSizeis 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-boot4ships the@RefreshScoperegistry 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, andread-onlyremoves 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 (notTimerConfigCustomizer), 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.adocstops at 2.3.0.
References
-
Resilience4j: CircuitBreaker, Retry, RateLimiter, Bulkhead and TimeLimiter
-
resilience4j-framework-commonat v2.4.0 (theCommon…ConfigurationProperties,CompositeCustomizerandConfigUtils) -
resilience4j-spring6at v2.4.0 (theXxxConfigurationclasses, the aspects and theSpelResolver) -
resilience4j-spring-boot4at v2.4.0 (the properties classes and the auto-configurations) -
RELEASENOTES.adocat v2.4.0 (through 2.3.0)