diff --git a/docs/02_concepts/11_timeouts.mdx b/docs/02_concepts/11_timeouts.mdx
index 55b937de..8b1c3a32 100644
--- a/docs/02_concepts/11_timeouts.mdx
+++ b/docs/02_concepts/11_timeouts.mdx
@@ -25,7 +25,7 @@ Every client method has a pre-assigned tier that matches the expected duration o
## Configuring default timeouts
-You can override the default values for each tier in the `ApifyClient` or `ApifyClientAsync` constructor. The `timeout_max` parameter sets an upper cap on the timeout for any individual API request, limiting exponential growth during retries.
+You can override the default values for each tier in the `ApifyClient` or `ApifyClientAsync` constructor. The `timeout_max` parameter caps the exponential timeout growth during retries. A base timeout that's already larger than `timeout_max`, whether an explicit `timedelta` or a tier configured above the cap, is honored as-is.
diff --git a/src/apify_client/_apify_client.py b/src/apify_client/_apify_client.py
index 391906c0..f15572dd 100644
--- a/src/apify_client/_apify_client.py
+++ b/src/apify_client/_apify_client.py
@@ -146,7 +146,7 @@ def __init__(
timeout_short: Default timeout for short-duration API operations (simple CRUD operations, ...).
timeout_medium: Default timeout for medium-duration API operations (batch operations, listing, ...).
timeout_long: Default timeout for long-duration API operations (long-polling, streaming, ...).
- timeout_max: Maximum timeout cap for exponential timeout growth across retries.
+ timeout_max: Caps exponential timeout growth across retries. A larger base timeout is honored, not clamped.
headers: Additional HTTP headers to include in all API requests.
compression: Compression algorithm for request bodies. Pass a string literal to select an algorithm,
or an `HttpCompressor` instance for finer-grained control.
@@ -508,7 +508,7 @@ def __init__(
timeout_short: Default timeout for short-duration API operations (simple CRUD operations, ...).
timeout_medium: Default timeout for medium-duration API operations (batch operations, listing, ...).
timeout_long: Default timeout for long-duration API operations (long-polling, streaming, ...).
- timeout_max: Maximum timeout cap for exponential timeout growth across retries.
+ timeout_max: Caps exponential timeout growth across retries. A larger base timeout is honored, not clamped.
headers: Additional HTTP headers to include in all API requests.
compression: Compression algorithm for request bodies. Pass a string literal to select an algorithm,
or an `HttpCompressor` instance for finer-grained control.
diff --git a/src/apify_client/http_clients/_base.py b/src/apify_client/http_clients/_base.py
index 7645228f..e669cdef 100644
--- a/src/apify_client/http_clients/_base.py
+++ b/src/apify_client/http_clients/_base.py
@@ -110,7 +110,7 @@ def __init__(
timeout_short: Default timeout for short-duration API operations (simple CRUD operations, ...).
timeout_medium: Default timeout for medium-duration API operations (batch operations, listing, ...).
timeout_long: Default timeout for long-duration API operations (long-polling, streaming, ...).
- timeout_max: Maximum timeout cap for exponential timeout growth across retries.
+ timeout_max: Caps exponential timeout growth across retries. A larger base timeout is honored, not clamped.
max_retries: Maximum number of retries for failed requests.
min_delay_between_retries: Minimum delay between retries.
statistics: Statistics tracker for API calls. Created automatically if not provided.
@@ -198,7 +198,8 @@ def _compute_timeout(self, timeout: Timeout, *, attempt: int) -> int | float | N
"""Resolve a timeout tier and compute the timeout for a request attempt with exponential increase.
For `no_timeout`, returns `None` to indicate no timeout. For tier literals and explicit `timedelta` values,
- doubles the timeout with each attempt but caps at `timeout_max`.
+ doubles the timeout with each attempt, capping exponential growth at `timeout_max` but never below the
+ resolved base timeout (so an explicit `timedelta` larger than `timeout_max` is honored).
Args:
timeout: The timeout specification to resolve (tier literal or explicit `timedelta`).
@@ -219,7 +220,9 @@ def _compute_timeout(self, timeout: Timeout, *, attempt: int) -> int | float | N
else:
resolved = timeout
- new_timeout = min(resolved * (2 ** (attempt - 1)), self._timeout_max)
+ # `timeout_max` caps exponential growth across retries, but must never shrink the resolved base
+ # timeout itself - an explicit `timedelta` larger than `timeout_max` overrides it for the call.
+ new_timeout = min(resolved * (2 ** (attempt - 1)), max(self._timeout_max, resolved))
return to_seconds(new_timeout)
def _prepare_request_call(
diff --git a/src/apify_client/http_clients/_impit.py b/src/apify_client/http_clients/_impit.py
index c3ef7212..b01a3c58 100644
--- a/src/apify_client/http_clients/_impit.py
+++ b/src/apify_client/http_clients/_impit.py
@@ -83,7 +83,7 @@ def __init__(
timeout_short: Default timeout for short-duration API operations (simple CRUD operations, ...).
timeout_medium: Default timeout for medium-duration API operations (batch operations, listing, ...).
timeout_long: Default timeout for long-duration API operations (long-polling, streaming, ...).
- timeout_max: Maximum timeout cap for exponential timeout growth across retries.
+ timeout_max: Caps exponential timeout growth across retries. A larger base timeout is honored, not clamped.
max_retries: Maximum number of retry attempts for failed requests.
min_delay_between_retries: Minimum delay between retries (increases exponentially with each attempt).
statistics: Statistics tracker for API calls. Created automatically if not provided.
@@ -332,7 +332,7 @@ def __init__(
timeout_short: Default timeout for short-duration API operations (simple CRUD operations, ...).
timeout_medium: Default timeout for medium-duration API operations (batch operations, listing, ...).
timeout_long: Default timeout for long-duration API operations (long-polling, streaming, ...).
- timeout_max: Maximum timeout cap for exponential timeout growth across retries.
+ timeout_max: Caps exponential timeout growth across retries. A larger base timeout is honored, not clamped.
max_retries: Maximum number of retry attempts for failed requests.
min_delay_between_retries: Minimum delay between retries (increases exponentially with each attempt).
statistics: Statistics tracker for API calls. Created automatically if not provided.
diff --git a/tests/unit/test_client_timeouts.py b/tests/unit/test_client_timeouts.py
index 2e87279e..47d29592 100644
--- a/tests/unit/test_client_timeouts.py
+++ b/tests/unit/test_client_timeouts.py
@@ -149,6 +149,24 @@ def test_compute_timeout_no_timeout_returns_none() -> None:
assert client._compute_timeout('no_timeout', attempt=1) is None
+def test_compute_timeout_explicit_timedelta_above_max_not_clamped() -> None:
+ """Test an explicit timedelta larger than timeout_max is honored, not clamped."""
+ client = ImpitHttpClient(timeout_max=timedelta(seconds=360))
+
+ assert client._compute_timeout(timedelta(minutes=30), attempt=1) == 1800.0
+ # Exponential growth stays bounded by the explicit timedelta itself.
+ assert client._compute_timeout(timedelta(minutes=30), attempt=2) == 1800.0
+
+
+def test_compute_timeout_tier_above_max_not_clamped() -> None:
+ """Test a configured tier larger than timeout_max is honored, not clamped."""
+ client = ImpitHttpClient(timeout_long=timedelta(seconds=600), timeout_max=timedelta(seconds=360))
+
+ assert client._compute_timeout('long', attempt=1) == 600.0
+ # Exponential growth stays bounded by the tier's base value itself.
+ assert client._compute_timeout('long', attempt=2) == 600.0
+
+
async def test_dynamic_timeout_async_client(monkeypatch: pytest.MonkeyPatch) -> None:
"""Tests timeout values for request with retriable errors.