Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
features:
- |
Add ``min`` parameter to ``wait_exponential_jitter`` to set a minimum wait
time floor, consistent with ``wait_exponential``. Also accept ``timedelta``
for ``max``, ``jitter``, and ``min`` parameters.
16 changes: 9 additions & 7 deletions tenacity/wait.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,26 +240,28 @@ def __call__(self, retry_state: "RetryCallState") -> float:
class wait_exponential_jitter(wait_base):
"""Wait strategy that applies exponential backoff and jitter.

It allows for a customized initial wait, maximum wait and jitter.
It allows for a customized initial wait, maximum wait, jitter and minimum.

This implements the strategy described here:
https://cloud.google.com/storage/docs/retry-strategy

The wait time is min(initial * 2**n + random.uniform(0, jitter), maximum)
The wait time is max(min, min(initial * 2**n + random.uniform(0, jitter), maximum))
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring formula hard-codes 2**n, but the implementation uses the configurable exp_base (self.exp_base ** (attempt_number - 1)). The docstring should reflect exp_base**n (or similar) to avoid misleading users who pass a non-default exp_base.

Suggested change
The wait time is max(min, min(initial * 2**n + random.uniform(0, jitter), maximum))
The wait time is max(min, min(initial * exp_base**n + random.uniform(0, jitter), maximum))

Copilot uses AI. Check for mistakes.
where n is the retry count.
"""

def __init__(
self,
initial: float = 1,
max: float = _utils.MAX_WAIT,
max: _utils.time_unit_type = _utils.MAX_WAIT,
exp_base: float = 2,
jitter: float = 1,
jitter: _utils.time_unit_type = 1,
min: _utils.time_unit_type = 0,
) -> None:
self.initial = initial
self.max = max
self.max = _utils.to_seconds(max)
self.exp_base = exp_base
self.jitter = jitter
self.jitter = _utils.to_seconds(jitter)
self.min = _utils.to_seconds(min)

def __call__(self, retry_state: "RetryCallState") -> float:
jitter = random.uniform(0, self.jitter)
Expand All @@ -268,4 +270,4 @@ def __call__(self, retry_state: "RetryCallState") -> float:
result = self.initial * exp + jitter
except OverflowError:
result = self.max
return max(0, min(result, self.max))
return max(max(0, self.min), min(result, self.max))
23 changes: 23 additions & 0 deletions tests/test_tenacity.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,29 @@ def test_wait_exponential_jitter(self) -> None:
fn = tenacity.wait_exponential_jitter()
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):
# Even for attempt 1 (base wait=1 + jitter 0..1 = 1..2), min=5 applies
self._assert_inclusive_range(fn(make_retry_state(1, 0)), 5, 5)
self._assert_inclusive_range(fn(make_retry_state(2, 0)), 5, 5)
self._assert_inclusive_range(fn(make_retry_state(3, 0)), 5, 5)
# For attempt 4, base wait=8 + jitter 0..1 = 8..9, above min
self._assert_inclusive_range(fn(make_retry_state(4, 0)), 8, 9)

def test_wait_exponential_jitter_timedelta(self) -> None:
from datetime import timedelta

fn = tenacity.wait_exponential_jitter(
max=timedelta(seconds=60),
jitter=timedelta(seconds=1),
min=timedelta(seconds=5),
)
for _ in range(1000):
self._assert_inclusive_range(fn(make_retry_state(1, 0)), 5, 5)
self._assert_inclusive_range(fn(make_retry_state(5, 0)), 16, 17)
self.assertEqual(fn(make_retry_state(7, 0)), 60)

def test_wait_retry_state_attributes(self) -> None:
class ExtractCallState(Exception):
pass
Expand Down
Loading