Skip to content

Retry and Timeout

Arnel Robles edited this page Sep 29, 2026 · 1 revision

Retry and timeout

Package: Carom. Classes: Carom, Bounce, Bounce<T>.

using Carom;

// Up to 3 retries, so at most 4 calls
var user = await Carom.ShotAsync(ct => api.GetUserAsync(id, ct), retries: 3);

// Same thing with a Bounce
var bounce = Bounce.Times(5)
    .WithDelay(TimeSpan.FromMilliseconds(200))
    .WithMaxDelay(TimeSpan.FromSeconds(5))
    .WithTimeout(TimeSpan.FromSeconds(30))
    .When(ex => ex is HttpRequestException);

var data = await Carom.ShotAsync(ct => api.FetchAsync(ct), bounce, cancellationToken);

Counting

retries is the number of retries, not attempts. Bounce.Times(3) makes up to 4 calls. A negative count is treated as 0.

Backoff

The delay between attempts uses decorrelated jitter: the next delay is a random value between the base delay and three times the previous delay, capped by MaxDelay.

Setting Default Notes
WithDelay 100 ms Negative throws ArgumentOutOfRangeException
WithMaxDelay 30 s Must be positive. A base delay above the cap is clamped to it
WithoutJitter off Fixed backoff of base times 2 to the power of the attempt, capped. Every client retries in step, so leave jitter on unless you have a reason

Which exceptions retry

Without When, every exception retries except OperationCanceledException. With When, only exceptions the predicate accepts retry. Cancellation never retries, whatever the predicate says. Bounce.On<TException>() is shorthand for When(ex => ex is TException).

Retrying on a result

var bounce = Bounce.For<HttpResponseMessage>(retries: 3)
    .WhenResult(r => (int)r.StatusCode >= 500);

var response = await Carom.ShotAsync(ct => http.GetAsync(url, ct), bounce);

When retries run out, the last result is returned, not an exception.

Timeout

WithTimeout covers the whole operation, every attempt and every backoff delay, not each attempt. When it fires you get TimeoutRejectedException, which has a Timeout property. When your own token is cancelled you get OperationCanceledException instead, so the two can be told apart.

Use the overloads that take Func<CancellationToken, Task>. They pass the linked token to your action, so the timeout cancels the work itself. With the overloads that take Func<Task>, Carom stops waiting when the timeout fires but cannot stop the operation, which keeps running in the background.

The timeout is async only. Passing a Bounce with a timeout to a synchronous Shot overload throws InvalidOperationException, because the sync path has no way to enforce it.

Allocations

Bounce is a readonly struct, and each fluent call returns a new copy. The Carom hot path allocates nothing beyond what your delegate captures. See Benchmarks.

Clone this wiki locally