Skip to main content
The @RetryPolicy annotation enables automatic retry with exponential backoff for failed handler invocations. When placed on a class alongside @ChangeStream, the framework retries the handler up to maxAttempts times with increasing delays between attempts.

Attributes

AttributeTypeDefaultDescription
maxAttemptsint3Maximum number of attempts (including the initial one)
initialDelayString"500ms"Delay before the first retry. Supports "500ms", "1s", "1m" format
maxDelayString"30s"Maximum delay cap
multiplierdouble2.0Multiplier applied to the delay between each retry
retryOnClass<? extends Throwable>[]{}Exception types that trigger a retry. Empty = all exceptions (except noRetryOn)
noRetryOnClass<? extends Throwable>[]See belowException types that should never trigger a retry
jitterbooleantrueAdds random variation (±20%) to the computed delay
maxAttempts includes the initial invocation. So maxAttempts = 3 means 1 initial attempt + 2 retries.

noRetryOn Defaults

By default, the following exceptions skip retry entirely — they are considered programming errors that retrying won’t fix:
  • IllegalArgumentException
  • NullPointerException
  • ClassCastException
noRetryOn always takes precedence over retryOn.

Backoff Algorithm

The delay between retries is computed using exponential backoff with an optional jitter:
baseDelay   = initialDelay × (multiplier ^ (attemptNumber - 1))
cappedDelay = min(baseDelay, maxDelay)

if jitter = true:
    finalDelay = cappedDelay × random(0.8, 1.2)    // ±20%
else:
    finalDelay = cappedDelay
Using the defaults (initialDelay = "500ms", multiplier = 2.0, maxDelay = "30s") and disabling jitter to show pure exponential growth, the delays are:
AttemptDelay
1 (initial)
2 (1st retry)500ms
3 (2nd retry)1s
42s
54s
68s
716s
8+30s (capped)

Head-of-Line Blocking

@RetryPolicy executes retries on the stream’s processing thread. While a handler is being retried, FlowWarden does not advance to the next event — the entire stream is held until the current event has succeeded, exhausted all retries, or returned ErrorAction.SKIP / ErrorAction.DLQ from an @OnError handler. A long retry sequence therefore blocks the stream for the sum of all backoff intervals (e.g. ~15 s with maxAttempts = 5, initialDelay = "1s", multiplier = 2.0). This is a deliberate trade-off preserving strict in-order delivery. See the retry guide for a worked example and the available workarounds.

See Also

Retry & DLQ Guide

Understand retry flows, exception filtering, tracking attempts, and best practices

@DeadLetterQueue

Store events that exhaust all retries for later reprocessing

@Checkpoint

Resume token persistence for reliable stream recovery

ChangeStreamContext

Runtime context including attempt number, event ID, and more