Retry with growing backoff

FxDart wins async

Requirement

The rate service is unavailable exactly twice, then serves. Retry with growing backoff — wait 40 ms after the first failure, 80 ms after the second — with a budget of three attempts. Print the payload, the attempt count, and the recorded backoff sequence (recorded when each wait is chosen, so the output is deterministic). Both versions must print the lines shown under Expected output.

Expected output
rates: EUR 0.85, GBP 0.74, JPY 148.20
attempts: 3
backoff: 40ms, 80ms

Side by side

RxDart

FxDart

Why they differ

Backoff is where the one-call symmetry of plain retry breaks. RxDart's retryWhen is a meta-stream protocol: for every error, your factory must return a notifier stream — emit a value to trigger the retry, emit an error to give up. So "wait 40 ms, then 80 ms" becomes mapping each failure to an Rx.timer stream, and because the factory only sees one error at a time, both the failure count and the attempt budget live in mutable variables outside the operator. It works, and it is maximally general — but you are hand-assembling a retry loop out of streams.

FxDart treats backoff as what it is: a number that depends on how many times you have failed. The delay hook on retry receives the failure count (1, 2, …) and returns a Duration — the whole policy is one expression, and the budget is the same attempts argument as before. Nothing about waiting between attempts requires a stream, and the pull side never pretends it does.

Verdict FxDart, on concept count: one hook versus a notifier-stream factory, an external counter, and a timer stream per failure.

Benchmark

Apple M1 Max, 32 GB RAM · Dart 3.12.2 (AOT-compiled) · 2026-08-18

Async case: the headline scale is N = 10,000, not 1,000,000. Every element costs an event-loop turn on both sides, so a million real awaits would measure Dart's event loop for minutes — not the pipeline. Delays are zero-length and the example's concurrency limit is kept; what the bars compare is the pipeline machinery.

N = 100

Time Tie

RxDart 687 µs
FxDart 575 µs

Peak memory Tie

RxDart 17.0 MB
FxDart 16.5 MB

N = 10,000

Time FxDart wins

RxDart 57.5 ms
FxDart 44.2 ms

Peak memory FxDart wins

RxDart 29.4 MB
FxDart 27.1 MB

Bars are medians of repeated timed iterations in fresh processes per side (small N is batched for timer resolution). Sides within 5% of each other — or within 0.6 ms, a difference no person can perceive — count as a tie; close relative races are re-measured up to 5 runs. In an app, anything under a few milliseconds is invisible to the user regardless of which bar is shorter. Memory is peak process RSS. The Dart VM and the dataset are identical on both sides, so the difference between the two bars is what the pipeline itself holds onto.