Fault Tolerance and Resilience
|
This section documents Quarkus 3.39.x (current) with 3.33.x LTS as the production recommendation, on the Java 17+ baseline, as published at the official Quarkus guides — which are the reference these pages are written and verified against. Quarkus 4 raises the baseline to Java 21 (first release candidate planned for end of September 2026, see the Quarkus blog); readers upgrading past 3.39.x/3.33.x LTS should expect that change. This content was generated with the assistance of AI and should be verified against those official guides before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
quarkus-smallrye-fault-tolerance implements the MicroProfile Fault Tolerance specification and adds several
SmallRye-specific extensions to it (@RateLimit, programmatic Guard`s). Every annotation on this page is a CDI
interceptor binding, so it applies to any CDI bean method exactly the same way whether that method backs a REST
resource, a REST client interface (already shown stacking `@Retry and
@CircuitBreaker on a client method), a messaging consumer, or a plain application service — nothing on this
page is specific to any one of those call sites. For the Spring Boot counterpart, see
Resilience Fundamentals.
./mvnw quarkus:add-extension -Dextensions=smallrye-fault-tolerance
@Retry
package com.example.orders;
import java.time.temporal.ChronoUnit;
import org.eclipse.microprofile.faulttolerance.Retry;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class InventoryClient {
@Retry(
maxRetries = 4,
delay = 200, delayUnit = ChronoUnit.MILLIS,
jitter = 100, jitterDelayUnit = ChronoUnit.MILLIS,
maxDuration = 5, durationUnit = ChronoUnit.SECONDS,
retryOn = { java.io.IOException.class },
abortOn = { IllegalArgumentException.class })
public InventoryLevel checkLevel(String sku) {
return fetchFromInventoryService(sku);
}
private InventoryLevel fetchFromInventoryService(String sku) {
// remote call
return null;
}
}
delay/jitter together produce a randomised back-off in [delay - jitter, delay + jitter] for each attempt — jitter exists specifically to stop many callers retrying a recovering downstream service in lockstep and
re-overwhelming it the instant it comes back. maxDuration bounds total time spent retrying regardless of
maxRetries, so a slow failure mode cannot keep retrying far longer than the caller can tolerate. retryOn
narrows retries to specific exception types (anything else propagates immediately); abortOn is the inverse — exceptions that should never be retried even if they match `retryOn’s type hierarchy, typically because they
indicate the request itself was wrong rather than that the call failed transiently.
@Timeout
import java.time.temporal.ChronoUnit;
import org.eclipse.microprofile.faulttolerance.Timeout;
@Timeout(value = 800, unit = ChronoUnit.MILLIS)
public InventoryLevel checkLevel(String sku) {
return fetchFromInventoryService(sku);
}
A method that has not returned within the given duration is interrupted and a org.eclipse.microprofile.faulttolerance.exceptions.TimeoutException
is raised in its place — this composes with @Retry (a timeout counts as a retryable failure by default) and
is usually the annotation that actually bounds an otherwise-unbounded blocking call, since neither @Retry nor
@CircuitBreaker alone limits how long a single attempt may run.
@Bulkhead
import org.eclipse.microprofile.faulttolerance.Bulkhead;
import org.eclipse.microprofile.faulttolerance.Asynchronous;
// semaphore bulkhead: bounds concurrent synchronous callers
@Bulkhead(5)
public InventoryLevel checkLevel(String sku) {
return fetchFromInventoryService(sku);
}
// thread-pool bulkhead: bounds concurrent async executions, with a bounded waiting queue
@Asynchronous
@Bulkhead(value = 5, waitingTaskQueue = 20)
public java.util.concurrent.CompletionStage<InventoryLevel> checkLevelAsync(String sku) {
return java.util.concurrent.CompletableFuture.completedFuture(fetchFromInventoryService(sku));
}
The semaphore form (the default, for a plain synchronous method) limits how many callers may be executing the
method at once — the (6+)`th concurrent caller is rejected immediately with a `BulkheadException rather than
queued. The thread-pool form applies only to @Asynchronous methods: it runs on a dedicated bounded thread
pool with its own bounded waitingTaskQueue, so excess work queues briefly instead of being rejected outright,
at the cost of an extra thread-pool hand-off. Either way, a bulkhead’s purpose is to stop one overloaded or
misbehaving downstream dependency from consuming every thread/permit in the caller and starving unrelated calls
that share the same process.
@CircuitBreaker
import java.time.temporal.ChronoUnit;
import org.eclipse.microprofile.faulttolerance.CircuitBreaker;
@CircuitBreaker(
requestVolumeThreshold = 10,
failureRatio = 0.5,
delay = 5, delayUnit = ChronoUnit.SECONDS,
successThreshold = 3)
public InventoryLevel checkLevel(String sku) {
return fetchFromInventoryService(sku);
}
requestVolumeThreshold is the minimum number of calls, within a rolling window of that same size, before the
circuit can trip at all — it exists so that a handful of early failures right after startup cannot open the
circuit on statistically meaningless data. Once at least that many calls have been made, failureRatio is
compared against the fraction of them that failed; crossing it opens the circuit. delay is how long the circuit
stays open before allowing a single probe call through (transitioning to half-open); successThreshold is how
many consecutive successful probe calls are needed, while half-open, before the circuit closes again — a single
failed probe reopens it immediately.
The state machine
While open, every call fails fast with a CircuitBreakerOpenException without ever invoking the method body — this is the actual point of a circuit breaker: giving a struggling downstream dependency a real window with
zero additional load, instead of every caller still hammering it while individually timing out.
Programmatic control: CircuitBreakerMaintenance
package com.example.orders;
import io.smallrye.faulttolerance.api.CircuitBreakerMaintenance;
import io.smallrye.faulttolerance.api.CircuitBreakerState;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class CircuitBreakerAdminService {
@Inject
CircuitBreakerMaintenance maintenance;
public CircuitBreakerState currentState() {
return maintenance.currentState("checkLevel-circuit");
}
public void forceReset() {
maintenance.resetAll();
}
}
io.smallrye.faulttolerance.api.CircuitBreakerMaintenance (a named circuit breaker requires
@CircuitBreakerName("checkLevel-circuit") alongside @CircuitBreaker to be addressable this way) lets an
operator inspect or forcibly reset circuit state from application code — an admin endpoint, a Quarkus command,
or a Dev UI extension — rather than waiting out delay or restarting the instance.
@Fallback
import org.eclipse.microprofile.faulttolerance.Fallback;
@Retry(maxRetries = 2)
@CircuitBreaker
@Fallback(fallbackMethod = "cachedLevel")
public InventoryLevel checkLevel(String sku) {
return fetchFromInventoryService(sku);
}
InventoryLevel cachedLevel(String sku) {
return InventoryLevel.unknown(sku);
}
fallbackMethod must match the original method’s signature exactly (same parameter types, same or a covariant
return type) and runs once every other configured strategy (retries exhausted, circuit open, timeout) has given
up — @Fallback is deliberately the outermost annotation in the execution order (see
Composing several strategies on one call), since a fallback should only ever run after every recovery
attempt has already failed.
@RateLimit
import java.time.temporal.ChronoUnit;
import io.smallrye.faulttolerance.api.RateLimit;
@RateLimit(value = 100, window = 1, windowUnit = ChronoUnit.SECONDS, minSpacing = 5, minSpacingUnit = ChronoUnit.MILLIS)
public InventoryLevel checkLevel(String sku) {
return fetchFromInventoryService(sku);
}
io.smallrye.faulttolerance.api.RateLimit is a SmallRye extension beyond the MicroProfile Fault Tolerance
specification: it caps a method to at most value invocations per window, rejecting any call beyond that with
a RateLimitException, and minSpacing additionally enforces a minimum gap between any two consecutive calls
regardless of the window count. Unlike @Bulkhead (which limits concurrent callers), @RateLimit limits
throughput over time — the two compose naturally when a downstream dependency has both a concurrency limit and
a calls-per-second quota.
@ApplyGuard and programmatic guards
package com.example.orders;
import java.time.Duration;
import io.smallrye.faulttolerance.api.ApplyGuard;
import io.smallrye.faulttolerance.api.FaultTolerance;
import io.smallrye.faulttolerance.api.Guard;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
@ApplicationScoped
public class InventoryGuards {
@Produces
@ApplicationScoped
public Guard inventoryGuard() {
return Guard.create()
.withRetry().maxRetries(3).delay(Duration.ofMillis(200)).done()
.withTimeout().duration(Duration.ofMillis(800)).done()
.withCircuitBreaker().requestVolumeThreshold(10).failureRatio(0.5).done()
.build();
}
@ApplyGuard("inventoryGuard")
public InventoryLevel checkLevel(String sku) {
return fetchFromInventoryService(sku);
}
}
io.smallrye.faulttolerance.api.Guard/FaultTolerance builds the same strategies programmatically instead of
through annotations — useful when the configuration itself needs to be computed or shared identically across
several methods without repeating annotation attributes on each one. @ApplyGuard("inventoryGuard") attaches a
named, CDI-produced Guard bean to a method, combining the ergonomics of the annotation-based interceptor with
programmatic construction.
Asynchronous and Mutiny support
Every annotation on this page applies to a method returning java.util.concurrent.CompletionStage/Future
(marked @Asynchronous) exactly as it does to a synchronous one, and quarkus-smallrye-fault-tolerance extends
this natively to Mutiny’s reactive types:
import io.smallrye.mutiny.Uni;
import io.smallrye.faulttolerance.api.RateLimit;
@Retry(maxRetries = 3)
@CircuitBreaker
@Timeout(500)
public Uni<InventoryLevel> checkLevelReactive(String sku) {
return fetchFromInventoryServiceReactive(sku);
}
A method returning Uni<T> needs no @Asynchronous annotation and no adapter — SmallRye Fault Tolerance
recognises the reactive return type and applies retry/timeout/circuit-breaker/bulkhead semantics to the
subscription rather than to a blocking call, which is what makes it safe to stack directly on reactive data
access (see Reactive Data Access) and reactive REST client calls
without introducing blocking behaviour.
Metrics integration
With quarkus-micrometer (or the OpenTelemetry bridge, see Observability)
also present, every fault-tolerance strategy emits its own meters without extra code — retry attempt counts,
circuit-breaker state transitions and time spent in each state, bulkhead rejections, and timeout occurrences,
each tagged with the method they apply to. This is what makes it practical to notice, say, a circuit breaker
flapping between closed and open in production instead of only inferring it from elevated error rates:
quarkus.fault-tolerance.metrics.enabled=true
Disabling fault tolerance
Fault tolerance can be turned off globally, which is occasionally useful for isolating whether an observed failure is caused by application logic or by a fault-tolerance strategy itself:
quarkus.fault-tolerance.enabled=false
Per the MicroProfile Fault Tolerance specification, every non-fallback strategy (retry, timeout, bulkhead,
circuit breaker) can instead be disabled while keeping fallbacks active, via the standard
MP_Fault_Tolerance_NonFallback_Enabled=false configuration property — useful for a test profile that wants a
fallback’s behaviour exercised without the delay a real retry/timeout would add. A single annotation on a single
method can also be disabled individually using the specification’s <fully-qualified-class-name>/<method-name>/<AnnotationType>/enabled=false
configuration key pattern, without touching the annotation itself in code.
Load shedding
Retries, bulkheads, and circuit breakers all protect a caller from a struggling downstream dependency; load shedding protects an application itself from being overwhelmed by more concurrent work than it can process, by proactively rejecting a portion of incoming requests once the system is past a healthy operating point rather than queueing them and letting latency balloon for everyone, including requests that arrive after the overload has already passed. This is a property of the HTTP layer serving inbound requests rather than of any one CDI-intercepted method, and the exact configuration surface is intentionally not duplicated here — see the dedicated load-shedding reference guide linked below for the current configuration keys and the algorithm behind them.
Service discovery and load balancing with SmallRye Stork
./mvnw quarkus:add-extension -Dextensions=smallrye-stork
Stork resolves a logical service name to a concrete instance list on the consumer side, so a client never
hardcodes a downstream URL — it was already introduced briefly on
the REST client page via the stork:// URL scheme; this section covers
the mechanism itself.
Service-discovery providers
# static list, for local development or a fixed set of instances
quarkus.stork.orders-service.service-discovery.type=static
quarkus.stork.orders-service.service-discovery.address-list=orders-1:8080,orders-2:8080
# Kubernetes: resolves instances from a Service/Endpoints object
quarkus.stork.orders-service.service-discovery.type=kubernetes
quarkus.stork.orders-service.service-discovery.k8s-namespace=orders
# Consul: resolves instances registered in Consul's service catalog
quarkus.stork.orders-service.service-discovery.type=consul
quarkus.stork.orders-service.service-discovery.consul-host=consul.example.com
quarkus.stork.orders-service.service-discovery.consul-port=8500
Stork itself only discovers instances — it does not register them. Registration is the responsibility of the
platform each provider queries: a Kubernetes Service is populated by kubelet health-checked Pod readiness, and
a Consul entry is registered by a Consul agent (or the application via Consul’s own registration API) running
alongside the instance, independently of Stork.
Load-balancer strategies
quarkus.stork.orders-service.load-balancer.type=round-robin
# or: random, least-response-time
Round-robin cycles evenly through resolved instances; random picks one uniformly at random per call (useful when many independent clients each doing round-robin could otherwise synchronise on the same instance); least-response-time tracks recent latency per instance and biases traffic toward the currently-fastest one, at the cost of more per-call bookkeeping than the other two strategies.
Integration with the REST client and gRPC
A stork://<service-name>/… URL is recognised anywhere a target host is configured — the REST client
(quarkus.rest-client."orders-api".url=stork://orders-service/orders, as shown on
the REST client page) and a gRPC client (quarkus.grpc.clients.orders.host=stork://orders-service,
see gRPC) both resolve through the exact same Stork configuration — the
discovery and load-balancing configuration is entirely independent of which protocol ultimately uses it.
Composing several strategies on one call
The MicroProfile Fault Tolerance specification fixes this order deliberately: @Fallback sits outermost so it
can catch a failure from any inner strategy, including a circuit that is currently open; @Retry wraps
@CircuitBreaker so a retried call can still be short-circuited without re-attempting a downstream call known to
be failing; @Timeout wraps @Bulkhead so a call already holding a bulkhead permit can still be aborted for
taking too long. A single failed attempt can therefore be, in order: rejected by the bulkhead, timed out, counted
toward the circuit breaker’s failure ratio, retried, and finally — only once retries are exhausted or the
circuit is open — resolved by the fallback.