youtube-transcript-api blocked: what the errors mean and how to fix them
The traceback ends in Could not retrieve a transcript for the video, the line after it says RequestBlocked or IpBlocked, and the same script ran fine at your desk an hour ago. Here is what each error is telling you, which ones a retry can fix, and the three ways out.
The exact errors the library raises
Every failure in youtube-transcript-api subclasses CouldNotRetrieveTranscript, whose message is Could not retrieve a transcript for the video {url}! followed by the specific cause. The cause line is the part that matters. Names and messages below are from the library's _errors.py.
| Exception | Message (start of) | What it means | Retry? |
|---|---|---|---|
RequestBlocked | YouTube is blocking requests from your IP. | The IP you fetch from is on a list YouTube treats as a bot source. Nearly always a cloud provider range. | Not from this IP. Change the exit address. |
IpBlocked | YouTube is blocking your requests, despite you using proxies. | You configured a proxy and the proxy's exit IP is flagged too. Common with datacenter proxies and cheap shared residential pools. | Rotate to a different exit; back off first. |
TranscriptsDisabled | Subtitles are disabled for this video | The uploader turned captions off. There is no track to read. | Never. Skip the video. |
NoTranscriptFound | No transcripts were found for any of the requested language codes | Captions exist, but not in the languages you asked for. The message lists what is available. | Only with a different language list. |
VideoUnavailable | The video is no longer available | Private, deleted, or region-blocked from where the request went out. | Never, unless the region is the cause. |
AgeRestricted | This video is age-restricted. Therefore, you are unable to retrieve transcripts for it without authenticating | YouTube wants a signed-in adult account before it serves the page. | Never without cookies from a logged-in session. |
Two more show up less often. VideoUnplayablecarries YouTube's own reason string. PoTokenRequired means YouTube asked for a proof-of-origin token that the library cannot mint, which is another way of saying the request looked automated. Treat it like a block.
The first two rows are about your IP. The other four are about the video. Retrying a video-side error burns proxy bandwidth and changes nothing; that is the single most common mistake in the logs we see.
Why it works on your laptop and fails on the server
The library reads the caption track through the same watch page your browser loads. YouTube serves that page freely to residential addresses and refuses it to address ranges it associates with bots, which in practice means every cloud provider. Your laptop is on a home ISP. Your container is on AWS, GCP, Azure, Hetzner, Fly, Railway or Render. The code is identical; the address is not.
The README is explicit: YouTube "has started blocking most IPs that are known to belong to cloud providers ... which means you will most likely run into RequestBlocked or IpBlockedexceptions when deploying your code to any cloud solutions."
A second trap: curl from the server returning a 200 for the watch URL. That does not prove the caption endpoint is reachable; the block applies to the caption fetch, not the HTML shell. The false positive, and why the official Data API is not a way around it, is covered in why you cannot download captions you do not own.
A third: exits in the EU intermittently get the cookie-consent wall instead of the page, which the library reports as a failed request. It comes and goes per IP, so it reads as flakiness.
Fix 1: a rotating residential proxy
This is what the library recommends and it works. Configure WebshareProxyConfig or GenericProxyConfig and requests go out through home connections that YouTube has no reason to flag.
from youtube_transcript_api import YouTubeTranscriptApi from youtube_transcript_api.proxies import WebshareProxyConfig ytt = YouTubeTranscriptApi( proxy_config=WebshareProxyConfig( proxy_username="...", proxy_password="...", retries_when_blocked=3, # default is 10; each retry re-downloads the page ) ) fetched = ytt.fetch("dQw4w9WgXcQ"
The cost reality: residential bandwidth is metered per gigabyte, and a transcript fetch pulls the whole watch page through the proxy. Compressed on the wire that is roughly 300 to 400 KB per successful fetch; the page is closer to 1.5 MB decompressed. A blocked attempt pays that again, and with the default of ten retries one stubborn video can cost 10 to 15 MB. At a few thousand transcripts a month it is tens of gigabytes. Cap the retries, pin the pool to one country to dodge the consent wall, and expect a second vendor on the invoice.
Fix 2: back off, rotate, and stop retrying the wrong things
If you already have a proxy and still see IpBlocked, the pool is partly burned. Do three things before you buy a bigger pool.
- Catch
TranscriptsDisabled,NoTranscriptFound,VideoUnavailableandAgeRestrictedseparately and never retry them. Log the video ID and move on. - Retry
RequestBlockedandIpBlockedwith exponential backoff and a hard cap, and make sure each attempt actually leaves through a different exit. A rotating gateway does this; a static list does not. - Spread fetches out. A burst of 200 requests through the same exit in a minute gets that exit flagged for everyone using it.
from youtube_transcript_api import ( RequestBlocked, IpBlocked, TranscriptsDisabled, NoTranscriptFound, VideoUnavailable, AgeRestricted, ) def fetch(video_id): for attempt in range(3): try: return ytt.fetch(video_id).to_raw_data() except (TranscriptsDisabled, NoTranscriptFound, VideoUnavailable, AgeRestricted) as e: return None # video-side: do not retry except (RequestBlocked, IpBlocked): time.sleep(2 ** attempt) # IP-side: back off, rotate raise RuntimeError("still blocked")
This keeps a working setup working. It does not make a burned pool unburned, and it does not help on the day YouTube changes the page format and every fetch fails until a new library version ships.
Fix 3: move the fetch to a hosted API
The third option is to stop owning the problem. TranscriptAPI runs the fetch through a residential pool pinned to one region, with the retry ceiling, the consent-wall handling and the parser upkeep on our side. You send an ID; you get segments in the same text / start / duration shape the library returns.
It is not free: one credit per transcript, 20 credits free on signup, from $10 for 2,500 after that (details on the pricing page). It also does not change what can be read: a video with captions off is still a video with captions off. What changes is that the blocked class of failures does not reach you. The full case for and against is on the youtube-transcript-api alternative page.
How TranscriptAPI maps these failures
We run the same class of fetch, so we see the same exceptions. Each one is translated into a status code, an error name, a plain-language hint, and a retryable flag you can branch on without parsing prose. The credit is refunded in every case.
| Library exception | Status | retryable | hint |
|---|---|---|---|
TranscriptsDisabled | 404 | false | Retrying will not help — the uploader has disabled captions for this video. |
NoTranscriptFound | 404 | false | Omit the language parameter to get whatever language the video has. |
VideoUnavailable | 404 | false | Check that the video is public. |
AgeRestricted | 404 | false | Retrying will not help. |
RequestBlocked / IpBlocked | 503 | true | Temporary. Retry in a few seconds; a fresh request goes out through a different route. |
| anything else upstream | 502 | true | Usually temporary. Retry in a few seconds. |
{
"detail": "This video has captions turned off, so there is no transcript to fetch. Your credits have been refunded.",
"error": "TranscriptsDisabled",
"hint": "Retrying will not help — the uploader has disabled captions for this video.",
"retryable": false,
"credits_refunded": true
}The rule for an automation is one line: 404 means skip, 5xx means retry with backoff. Status codes and the refund policy are documented in the API reference.
The same fetch, hosted
curl "https://api.transcriptapi.io/transcript?video_id=dQw4w9WgXcQ" \ -H "Authorization: Bearer ta_..."
import requests r = requests.get( "https://api.transcriptapi.io/transcript", params={"video_id": "dQw4w9WgXcQ"}, headers={"Authorization": "Bearer ta_..."}, timeout=60, ) if r.status_code == 404: skip(r.json()["hint"]) # retryable: false elif r.status_code >= 500: retry_later() # refunded already else: segments = r.json()["transcript"]
Porting an existing codebase, including the older get_transcript() form, is walked through in the migration guide.
Sign in for 20 free credits, no card, and run the failing ID through /transcript. If it comes back 404, the video was never readable and the proxy was not your problem. If it comes back 200, it was.
Frequently asked questions
Why does youtube-transcript-api say RequestBlocked on my server but not on my laptop?
Your laptop sits on a residential IP; the server sits on a cloud provider range that YouTube treats as a bot source. Same code, same video, different address. The library's README states that deployments to AWS, GCP, Azure and similar will most likely hit RequestBlocked or IpBlocked.
Does a VPN or a datacenter proxy fix IpBlocked?
Usually not for long. Datacenter proxy ranges are just as well known to YouTube as the cloud ranges you are already on. The library's own recommendation is rotating residential proxies, which are billed per gigabyte and cost real money at volume.
Is 'Could not retrieve a transcript for the video' the same as being blocked?
No. It is the base class message every failure shares. The line after it names the real cause: RequestBlocked or IpBlocked means your IP, TranscriptsDisabled or NoTranscriptFound means the video has no usable captions, VideoUnavailable or AgeRestricted means you cannot reach the page at all. Read the second line before you retry.
Should I retry TranscriptsDisabled or VideoUnavailable?
No. Those describe the video, not the connection, and a retry returns the same answer while spending proxy bandwidth. Retry only the blocked errors, with backoff and a fresh exit IP, and cap the attempts.
How does TranscriptAPI handle the same failures?
Every failure is returned with an error name, a plain-language hint, and retryable: true or false. Video-side problems are a 404 with retryable: false; a block on our side is a 503 with retryable: true. The credit is refunded in both cases. Retry the 503s with backoff and skip the 404s.
Can TranscriptAPI read age-restricted or private videos?
No. Nobody can without the owner's account. Those come back as a 404 with the reason in the hint, refunded. What we remove is the blocking class of failures: the proxies, the retries and the format changes are on our side.