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-Afterfirst. 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 anhttpxclient) 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
429at 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
429exactly 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.