Skip to main content

Retries and Timeouts

The Spotify SDK includes built-in retry logic with exponential backoff and jitter to handle transient errors, rate limits, and network issues automatically.

Overview

The SDK automatically retries requests that fail due to:
  • Connection errors - Network connectivity issues
  • Timeout exceptions - Requests that exceed the timeout limit
  • Rate limit responses (429) - Respects the Retry-After header from Spotify
  • Server errors (5xx) - Temporary Spotify server issues
Client errors (4xx) like NotFoundError, BadRequestError, and AuthenticationError are not retried automatically, as they typically indicate an issue with the request that won’t be resolved by retrying.

Default Retry Configuration

Both SpotifyClient and AsyncSpotifyClient use these default retry settings:
SettingDefault ValueDescription
max_retries3Maximum number of retry attempts
INITIAL_BACKOFF0.5 secondsStarting backoff delay
MAX_BACKOFF8.0 secondsMaximum backoff delay
BACKOFF_MULTIPLIER2Multiplier for exponential backoff
These values are defined in the AsyncBaseClient class (src/spotify_sdk/_async/_base_client.py:28-32):
class AsyncBaseClient:
    # Retry configuration
    MAX_RETRIES = 3
    INITIAL_BACKOFF = 0.5
    MAX_BACKOFF = 8.0
    BACKOFF_MULTIPLIER = 2

Configuring Max Retries

You can customize the maximum number of retries when initializing the client:
from spotify_sdk import SpotifyClient

client = SpotifyClient(
    access_token="your-access-token",
    max_retries=5,  # Increase to 5 retry attempts
)

Exponential Backoff with Jitter

The SDK uses exponential backoff with jitter to avoid overwhelming the API and prevent thundering herd problems.

How It Works

1
Calculate Base Backoff
2
The backoff time doubles with each retry attempt:
3
  • Attempt 1: 0.5 seconds
  • Attempt 2: 1.0 seconds
  • Attempt 3: 2.0 seconds
  • Attempt 4: 4.0 seconds
  • Attempt 5: 8.0 seconds (capped at MAX_BACKOFF)
  • 4
    Apply Jitter
    5
    A random multiplier between 0.5 and 1.0 is applied to prevent synchronized retries from multiple clients.
    6
    Sleep and Retry
    7
    The client waits for the calculated backoff time before retrying the request.
    The backoff calculation is implemented in _calculate_backoff() (src/spotify_sdk/_async/_base_client.py:221-228):
    def _calculate_backoff(self, attempt: int) -> float:
        """Calculate backoff time with jitter."""
        backoff = min(
            self.INITIAL_BACKOFF * (self.BACKOFF_MULTIPLIER**attempt),
            self.MAX_BACKOFF,
        )
        # Add jitter (0.5 to 1.0 multiplier)
        return backoff * (0.5 + random.random() * 0.5)
    

    Example Backoff Times

    Here’s what actual backoff times might look like with jitter:
    AttemptBase DelayWith Jitter (range)
    10.5s0.25s - 0.5s
    21.0s0.5s - 1.0s
    32.0s1.0s - 2.0s
    44.0s2.0s - 4.0s
    58.0s4.0s - 8.0s
    Jitter helps distribute retry attempts over time, reducing the likelihood of multiple clients retrying simultaneously and causing another spike in traffic.

    Rate Limit Handling

    When Spotify returns a 429 (rate limit) response, the SDK respects the Retry-After header provided by the API.

    Rate Limit Retry Logic

    The retry logic for rate limits (src/spotify_sdk/_async/_base_client.py:146-154):
    except RateLimitError as e:
        last_exception = e
        if attempt < retries:
            sleep_time = e.retry_after or self._calculate_backoff(
                attempt
            )
            await anyio.sleep(sleep_time)
            continue
        raise
    
    The SDK will:
    1. Use the Retry-After value from the response header if available
    2. Fall back to exponential backoff if no Retry-After header is present
    3. Retry the request after waiting
    4. Raise RateLimitError if all retries are exhausted

    Handling Rate Limits in Your Code

    from spotify_sdk import SpotifyClient, RateLimitError
    
    client = SpotifyClient(access_token="your-access-token")
    
    try:
        album = client.albums.get("5K79FLRUCSysQnVESLcTdb")
    except RateLimitError as e:
        print(f"Rate limited! Retry after {e.retry_after} seconds")
        print(f"Status: {e.status_code}")
        print(f"Message: {e.message}")
    

    Timeout Configuration

    The SDK allows you to configure request timeouts at the client level and per-request.

    Client-Level Timeout

    Set a default timeout for all requests:
    from spotify_sdk import SpotifyClient
    
    client = SpotifyClient(
        access_token="your-access-token",
        timeout=60.0,  # 60 second timeout for all requests
    )
    

    Per-Request Timeout

    The underlying request() method supports per-request timeout overrides (src/spotify_sdk/_async/_base_client.py:90-110):
    async def request(
        self,
        method: str,
        path: str,
        params: dict[str, Any] | None = None,
        json: dict[str, Any] | None = None,
        headers: dict[str, str] | None = None,
        content: str | bytes | None = None,
        timeout: float | None = None,  # Override timeout per request
        max_retries: int | None = None,
    ) -> Any:
    
    Timeout exceptions are automatically retried up to max_retries times. If you’re experiencing consistent timeouts, consider increasing the timeout value rather than just increasing retries.

    Retry Flow Diagram

    Here’s how the retry logic works:
    Request Attempt
         |
         v
    [Make API Call]
         |
         +---> Success? --> Return Response
         |
         +---> Connection/Timeout Error?
         |          |
         |          v
         |     [Calculate Backoff]
         |          |
         |          v
         |     [Sleep with Jitter]
         |          |
         |          v
         |     Attempts < max_retries? --> Retry Request
         |          |
         |          v
         |     Raise SpotifyError
         |
         +---> Rate Limit (429)?
         |          |
         |          v
         |     [Use Retry-After or Backoff]
         |          |
         |          v
         |     [Sleep]
         |          |
         |          v
         |     Attempts < max_retries? --> Retry Request
         |          |
         |          v
         |     Raise RateLimitError
         |
         +---> Server Error (5xx)?
         |          |
         |          v
         |     [Calculate Backoff]
         |          |
         |          v
         |     [Sleep with Jitter]
         |          |
         |          v
         |     Attempts < max_retries? --> Retry Request
         |          |
         |          v
         |     Raise ServerError
         |
         +---> Client Error (4xx)? --> Raise Specific Error
    

    Which Errors Trigger Retries

    Retried Automatically

    These errors are automatically retried:
    import httpx
    
    # These are caught and retried
    try:
        response = await client.request("GET", "/albums/123")
    except httpx.ConnectError:
        # Automatically retried
        pass
    

    Not Retried

    These errors are not retried automatically:
    from spotify_sdk import (
        AuthenticationError,  # 401 - Invalid token
        BadRequestError,      # 400 - Invalid request
        ForbiddenError,       # 403 - Insufficient permissions
        NotFoundError,        # 404 - Resource not found
    )
    
    # These are raised immediately without retries
    try:
        album = client.albums.get("invalid_id")
    except NotFoundError:
        # Not retried - the ID is invalid
        pass
    

    Implementation Details

    The retry logic is implemented in the request() method of AsyncBaseClient (src/spotify_sdk/_async/_base_client.py:122-163):
    for attempt in range(retries + 1):
        try:
            access_token = await self._get_access_token()
            request_headers = self._default_headers(access_token)
            if headers:
                request_headers.update(headers)
            response = await self._http_client.request(
                method=method,
                url=path,
                params=self._clean_params(params),
                json=json,
                content=content,
                timeout=timeout,
                headers=request_headers,
            )
            return self._handle_response(response)
    
        except (httpx.ConnectError, httpx.TimeoutException) as e:
            last_exception = e
            if attempt < retries:
                await anyio.sleep(self._calculate_backoff(attempt))
                continue
            raise SpotifyError(f"Connection error: {e}") from e
    
        except RateLimitError as e:
            last_exception = e
            if attempt < retries:
                sleep_time = e.retry_after or self._calculate_backoff(
                    attempt
                )
                await anyio.sleep(sleep_time)
                continue
            raise
    
        except ServerError as e:
            last_exception = e
            if attempt < retries:
                await anyio.sleep(self._calculate_backoff(attempt))
                continue
            raise
    

    Best Practices

    For production applications, the default max_retries=3 is usually sufficient. For batch processing or less time-sensitive operations, you might increase this:
    # Production API - keep default
    client = SpotifyClient(access_token="token", max_retries=3)
    
    # Batch processing - can tolerate longer retry periods
    client = SpotifyClient(access_token="token", max_retries=5)
    
    # Critical path - fail fast
    client = SpotifyClient(access_token="token", max_retries=1)
    
    If you’re on a slow network or making requests that return large responses, increase the timeout:
    client = SpotifyClient(
        access_token="token",
        timeout=60.0,  # 60 seconds for slow connections
    )
    
    While retries happen automatically, you may want to log when they occur for monitoring:
    import logging
    from spotify_sdk import SpotifyClient, RateLimitError, ServerError
    
    logger = logging.getLogger(__name__)
    
    client = SpotifyClient(access_token="token", max_retries=3)
    
    try:
        album = client.albums.get("5K79FLRUCSysQnVESLcTdb")
    except RateLimitError as e:
        logger.warning(f"Rate limited after retries: {e.retry_after}s")
        raise
    except ServerError as e:
        logger.error(f"Server error after retries: {e.message}")
        raise
    
    When all retries are exhausted, the SDK raises an exception. Handle this in your application:
    from spotify_sdk import SpotifyClient, ServerError
    import time
    
    client = SpotifyClient(access_token="token", max_retries=3)
    
    max_attempts = 3
    for attempt in range(max_attempts):
        try:
            album = client.albums.get("5K79FLRUCSysQnVESLcTdb")
            break
        except ServerError:
            if attempt < max_attempts - 1:
                time.sleep(10)  # Wait before trying again
                continue
            raise  # All attempts failed
    

    Build docs developers (and LLMs) love