diff --git a/src/TeamsISO.Engine/Pipeline/ExponentialBackoff.cs b/src/TeamsISO.Engine/Pipeline/ExponentialBackoff.cs
new file mode 100644
index 0000000..5e6da15
--- /dev/null
+++ b/src/TeamsISO.Engine/Pipeline/ExponentialBackoff.cs
@@ -0,0 +1,46 @@
+namespace TeamsISO.Engine.Pipeline;
+
+///
+/// Exponential backoff policy used by 's restart supervisor.
+/// Doubles the delay on each successive attempt and caps it at cap. After
+/// maxAttempts consecutive failures, returns true.
+///
+public sealed class ExponentialBackoff
+{
+ private readonly int _maxAttempts;
+ private readonly TimeSpan _initial;
+ private readonly TimeSpan _cap;
+
+ public ExponentialBackoff(int maxAttempts, TimeSpan initial, TimeSpan cap)
+ {
+ if (maxAttempts <= 0) throw new ArgumentOutOfRangeException(nameof(maxAttempts));
+ if (initial <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(initial));
+ if (cap < initial) throw new ArgumentOutOfRangeException(nameof(cap));
+ _maxAttempts = maxAttempts;
+ _initial = initial;
+ _cap = cap;
+ }
+
+ /// Default policy: 5 attempts, 1s initial, 30s cap.
+ public static ExponentialBackoff Default { get; } =
+ new(maxAttempts: 5, initial: TimeSpan.FromSeconds(1), cap: TimeSpan.FromSeconds(30));
+
+ public int MaxAttempts => _maxAttempts;
+
+ ///
+ /// Returns the delay before the -th retry.
+ /// is 1-based.
+ ///
+ public TimeSpan NextDelay(int attempt)
+ {
+ if (attempt <= 0) throw new ArgumentOutOfRangeException(nameof(attempt));
+ var multiplier = Math.Pow(2, attempt - 1);
+ var delaySeconds = Math.Min(_initial.TotalSeconds * multiplier, _cap.TotalSeconds);
+ return TimeSpan.FromSeconds(delaySeconds);
+ }
+
+ ///
+ /// Returns true once exceeds maxAttempts.
+ ///
+ public bool ShouldGiveUp(int attemptCount) => attemptCount > _maxAttempts;
+}
diff --git a/src/tests/TeamsISO.Engine.Tests/Pipeline/ExponentialBackoffTests.cs b/src/tests/TeamsISO.Engine.Tests/Pipeline/ExponentialBackoffTests.cs
new file mode 100644
index 0000000..f182fba
--- /dev/null
+++ b/src/tests/TeamsISO.Engine.Tests/Pipeline/ExponentialBackoffTests.cs
@@ -0,0 +1,31 @@
+using TeamsISO.Engine.Pipeline;
+
+namespace TeamsISO.Engine.Tests.Pipeline;
+
+public class ExponentialBackoffTests
+{
+ [Theory]
+ [InlineData(1, 1)]
+ [InlineData(2, 2)]
+ [InlineData(3, 4)]
+ [InlineData(4, 8)]
+ [InlineData(5, 16)]
+ [InlineData(6, 30)] // capped at 30s
+ [InlineData(7, 30)]
+ public void NextDelay_FollowsExponentialSequenceWithCap(int attempt, int expectedSeconds)
+ {
+ var policy = new ExponentialBackoff(maxAttempts: 5, initial: TimeSpan.FromSeconds(1), cap: TimeSpan.FromSeconds(30));
+ policy.NextDelay(attempt).Should().Be(TimeSpan.FromSeconds(expectedSeconds));
+ }
+
+ [Theory]
+ [InlineData(1, false)]
+ [InlineData(4, false)]
+ [InlineData(5, false)] // 5 attempts allowed
+ [InlineData(6, true)] // give up after the 5th
+ public void ShouldGiveUp_AfterMaxAttempts(int attemptCount, bool expectGiveUp)
+ {
+ var policy = new ExponentialBackoff(maxAttempts: 5, initial: TimeSpan.FromSeconds(1), cap: TimeSpan.FromSeconds(30));
+ policy.ShouldGiveUp(attemptCount).Should().Be(expectGiveUp);
+ }
+}