Skip to content
Open
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
2 changes: 1 addition & 1 deletion docs/02_concepts/11_timeouts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ApiLink to="class/ApifyClient">`ApifyClient`</ApiLink> or <ApiLink to="class/ApifyClientAsync">`ApifyClientAsync`</ApiLink> 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 <ApiLink to="class/ApifyClient">`ApifyClient`</ApiLink> or <ApiLink to="class/ApifyClientAsync">`ApifyClientAsync`</ApiLink> 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.

<Tabs>
<TabItem value="AsyncExample" label="Async client" default>
Expand Down
4 changes: 2 additions & 2 deletions src/apify_client/_apify_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
9 changes: 6 additions & 3 deletions src/apify_client/http_clients/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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`).
Expand All @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions src/apify_client/http_clients/_impit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions tests/unit/test_client_timeouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Loading