From 1316a12bd59500fb8fd7f48a78baeee73093f51c Mon Sep 17 00:00:00 2001 From: Samuele Fiorini Date: Wed, 4 Mar 2026 22:13:53 +0100 Subject: [PATCH 1/4] feat: add new `wait_fibonacci` strategy for Fibonacci backoff. --- tenacity/__init__.py | 2 ++ tenacity/wait.py | 45 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/tenacity/__init__.py b/tenacity/__init__.py index 282e6dae..654b39db 100644 --- a/tenacity/__init__.py +++ b/tenacity/__init__.py @@ -75,6 +75,7 @@ wait_exception, wait_exponential, wait_exponential_jitter, + wait_fibonacci, wait_fixed, wait_incrementing, wait_none, @@ -797,6 +798,7 @@ def wrap(f: t.Callable[P, R]) -> _RetryDecorated[P, R]: "wait_exception", "wait_exponential", "wait_exponential_jitter", + "wait_fibonacci", "wait_fixed", "wait_full_jitter", "wait_incrementing", diff --git a/tenacity/wait.py b/tenacity/wait.py index b28b7886..e60cbb2b 100644 --- a/tenacity/wait.py +++ b/tenacity/wait.py @@ -206,6 +206,51 @@ def __call__(self, retry_state: "RetryCallState") -> float: return max(max(0, self.min), min(result, self.max)) +class wait_fibonacci(wait_base): + """Wait strategy that applies a Fibonacci backoff. + + Fibonacci backoff is a compromise between linear and exponential backoff. + It increases the wait time more slowly than a pure exponential strategy, + making it effective for scenarios where a resource might be unavailable + for a moderate duration and you want to minimize latency without + hammering the server. + + .. note:: + This strategy is stateful. The wait intervals increase with each + subsequent retry performed by this specific instance. + + Example:: + + # Wait times: 1s, 2s, 3s, 5s, 8s, 13s... up to 60s + wait_fibonacci(max=60) + """ + + def __init__( + self, + max: _utils.time_unit_type = _utils.MAX_WAIT, + ) -> None: + """Initialize the wait strategy. + + :param max: The maximum number of seconds to wait. + """ + self.max = _utils.to_seconds(max) + self._serie = [0, 1] + + def compute_next_value(self) -> int: + """Compute the next value in the Fibonacci sequence.""" + next_value = self._serie[-1] + self._serie[-2] + self._serie.append(next_value) + return next_value + + def __call__(self, retry_state: "RetryCallState") -> float: + """Return the next wait time from the Fibonacci sequence.""" + try: + result = self.compute_next_value() + except OverflowError: + return self.max + return min(result, self.max) + + class wait_random_exponential(wait_exponential): """Random wait with exponentially widening window. From 3ab64f62862435b101a4ee88a425233fcce31067 Mon Sep 17 00:00:00 2001 From: Samuele Fiorini Date: Wed, 4 Mar 2026 22:28:20 +0100 Subject: [PATCH 2/4] feat: add comprehensive tests for the `wait_fibonacci` retry strategy, including `max` parameter and stateful behavior across multiple invocations. --- tests/test_tenacity.py | 49 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/test_tenacity.py b/tests/test_tenacity.py index efefba8c..1cd7f436 100644 --- a/tests/test_tenacity.py +++ b/tests/test_tenacity.py @@ -617,6 +617,55 @@ def test_wait_exponential_jitter(self) -> None: fn = tenacity.wait_exponential_jitter() fn(make_retry_state(0, 0)) + def test_wait_fibonacci(self) -> None: + # wait_fibonacci is purely stateful: it ignores retry_state and + # advances its internal Fibonacci sequence on each call. + fn = tenacity.wait_fibonacci() + expected = [1, 2, 3, 5, 8, 13, 21, 34] + for i, exp in enumerate(expected, 1): + self.assertEqual(fn(make_retry_state(i, 0)), exp) + + def test_wait_fibonacci_with_max_wait(self) -> None: + for max_ in (10, datetime.timedelta(seconds=10)): + with self.subTest(): + fn = tenacity.wait_fibonacci(max=max_) + # 1, 2, 3, 5, 8, 10, 10, 10 + self.assertEqual(fn(make_retry_state(1, 0)), 1) + self.assertEqual(fn(make_retry_state(2, 0)), 2) + self.assertEqual(fn(make_retry_state(3, 0)), 3) + self.assertEqual(fn(make_retry_state(4, 0)), 5) + self.assertEqual(fn(make_retry_state(5, 0)), 8) + self.assertEqual(fn(make_retry_state(6, 0)), 10) + self.assertEqual(fn(make_retry_state(7, 0)), 10) + self.assertEqual(fn(make_retry_state(8, 0)), 10) + + def test_wait_fibonacci_multiple_invocations(self) -> None: + sleep_intervals: list[float] = [] + r = Retrying( + sleep=sleep_intervals.append, + wait=tenacity.wait_fibonacci(max=60), + stop=tenacity.stop_after_attempt(6), + retry=tenacity.retry_if_result(lambda x: x == 1), + ) + + @r.wraps + def always_return_1() -> int: + return 1 + + self.assertRaises(tenacity.RetryError, always_return_1) + self.assertEqual(sleep_intervals, [1, 2, 3, 5, 8]) + sleep_intervals[:] = [] + + # Second invocation: wait_fibonacci is stateful, sequence continues. + # The first run consumed 6 Fibonacci values (5 sleeps + 1 extra wait + # call on the final attempt), so the second run picks up at position 7. + self.assertRaises(tenacity.RetryError, always_return_1) + self.assertEqual(sleep_intervals, [21, 34, 55, 60, 60]) + + def test_wait_fibonacci_default_args(self) -> None: + fn = tenacity.wait_fibonacci() + fn(make_retry_state(0, 0)) + def test_wait_retry_state_attributes(self) -> None: class ExtractCallState(Exception): pass From b30b080082ae34fac1ee23eac277746ba1743858 Mon Sep 17 00:00:00 2001 From: Samuele Fiorini Date: Wed, 4 Mar 2026 22:35:09 +0100 Subject: [PATCH 3/4] feat: Add `wait_fibonacci` strategy and its documentation to enable Fibonacci backoff. --- doc/source/index.rst | 11 +++++++++++ .../notes/wait-fibonacci-55c6d584f5a34432.yaml | 6 ++++++ 2 files changed, 17 insertions(+) create mode 100644 releasenotes/notes/wait-fibonacci-55c6d584f5a34432.yaml diff --git a/doc/source/index.rst b/doc/source/index.rst index ba21fd93..59062d3d 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -201,6 +201,17 @@ increasing jitter helps minimise collisions. raise Exception +For a gentler backoff curve than pure exponential, Fibonacci backoff increases +wait times following the Fibonacci sequence (1, 2, 3, 5, 8, 13, …). + +.. testcode:: + + @retry(wait=wait_fibonacci(max=60)) + def wait_fibonacci_up_to_60s(): + print("Wait 1s, 2s, 3s, 5s, 8s, 13s, ... up to 60s between retries") + raise Exception + + Sometimes it's necessary to build a chain of backoffs. .. testcode:: diff --git a/releasenotes/notes/wait-fibonacci-55c6d584f5a34432.yaml b/releasenotes/notes/wait-fibonacci-55c6d584f5a34432.yaml new file mode 100644 index 00000000..df5199c4 --- /dev/null +++ b/releasenotes/notes/wait-fibonacci-55c6d584f5a34432.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add ``wait_fibonacci`` strategy that applies Fibonacci backoff. + This is a stateful wait strategy where wait times follow the Fibonacci + sequence (1, 2, 3, 5, 8, 13, ...) up to an optional maximum. From 14082e96f0230afeb4c1da2bdfe8e638eb42da32 Mon Sep 17 00:00:00 2001 From: Samuele Fiorini Date: Thu, 12 Mar 2026 09:11:41 +0100 Subject: [PATCH 4/4] style: add blank line in test_wait_fibonacci_default_args. --- tests/test_tenacity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_tenacity.py b/tests/test_tenacity.py index 00592b03..4f4a086e 100644 --- a/tests/test_tenacity.py +++ b/tests/test_tenacity.py @@ -666,6 +666,7 @@ def always_return_1() -> int: def test_wait_fibonacci_default_args(self) -> None: fn = tenacity.wait_fibonacci() fn(make_retry_state(0, 0)) + def test_wait_exponential_jitter_min(self) -> None: fn = tenacity.wait_exponential_jitter(initial=1, max=60, jitter=1, min=5) for _ in range(1000):