Note · June 20, 2026

How to handle Spotify API rate limits in Python (or skip them)

The official Spotify Web API returns 429s when you push too hard. Here's how to back off correctly — and how to avoid the limit entirely for public data.

If you’ve written anything against the official Spotify Web API that loops over more than a handful of items, you’ve met 429 Too Many Requests. It usually shows up right when the script was finally working.

There are only two honest ways through it: handle the limit properly, or — for public data — skip the API that imposes it. Here’s both.

Why you’re seeing 429s

Spotify’s Web API enforces a rolling rate limit per app. Go over it and every call comes back 429, with a Retry-After header telling you how many seconds to wait. Spotify doesn’t publish the exact ceiling, and it varies by app and endpoint — so you can’t “stay just under it” with a hardcoded delay. You have to react to the 429 when it actually arrives.

Handle it: respect Retry-After, then back off

The only robust pattern is to read Retry-After, wait, and retry — with exponential backoff as a fallback when the header is missing:

import time
import requests

def get_with_backoff(url, headers, max_retries=5):
    for attempt in range(max_retries):
        resp = requests.get(url, headers=headers, timeout=10)
        if resp.status_code != 429:
            resp.raise_for_status()
            return resp.json()
        # Spotify tells you exactly how long to wait; trust it over a guess.
        wait = int(resp.headers.get("Retry-After", 2 ** attempt))
        time.sleep(wait)
    raise RuntimeError(f"Still rate-limited after {max_retries} retries")

A few things that actually matter:

  • Honor Retry-After first. Your own backoff math is a fallback for when it’s absent, not a replacement.
  • One session, not one-per-request. Reuse a requests.Session() (or an httpx client) so connections and the limit are accounted for in one place.
  • Don’t fan out past the limit. Twenty threads hammering the API just means twenty things sleeping on 429 at once. Concurrency doesn’t buy you a higher ceiling.

Or skip it: public data isn’t behind that limit

Here’s the part worth knowing before you build a whole retry harness. A lot of what people reach for the API to get — track and album metadata, cover art, the 30-second preview — is already public on Spotify’s web player, which doesn’t require an app token, OAuth, or that per-app rate limit at all.

SpotifyScraper reads it directly. No client ID, no 429 dance:

from spotify_scraper import SpotifyClient

with SpotifyClient() as client:
    track = client.get_track("https://open.spotify.com/track/0VjIjW4GlUZAMYd2vXMi3b")
    print(track.name, "—", track.artists[0].name)
    print(track.preview_url)

For a batch, the async client overlaps the network instead of resolving links one at a time — though it’s still someone’s server, so stay reasonable:

import asyncio
from spotify_scraper import AsyncSpotifyClient

async def main(urls):
    async with AsyncSpotifyClient() as client:
        tracks = await asyncio.gather(*(client.get_track(u) for u in urls))
        return [t.name for t in tracks]

asyncio.run(main(urls))

Which one you actually want

Be honest with yourself about the data:

  • Private or user-scoped — playback state, someone’s library, personalized recommendations — only the official API has it. Use it, and handle 429 exactly as above.
  • Public metadata, previews, and cover art — skipping the API removes the rate-limit problem instead of managing it, because you were never inside the rate-limited surface to begin with.

Either way, the considerate move is the same: cache what you’ve already fetched, and don’t ask for the same field twice.

If the second path is the one you want: try it in the browser first, then pip install spotifyscraper or read the source on GitHub.

boiler room — ali@aliakhtari.com