RequestBlocked / IpBlocked in youtube-transcript-api — what actually fixes it
Your script works on your laptop and dies the moment it is deployed. That is not a bug in your code, and it is not something a longer sleep() will solve. Here is the actual mechanism, ranked by whether it fixes anything.
The 30-second version
- Both exceptions mean the same thing: YouTube decided the request did not come from a person, and refused.
IpBlockedis the address;RequestBlockedis the request. In practice you cannot act on the difference. - It is about where you run, not what you run. The library's own README states that YouTube "has started blocking most IPs that are known to belong to cloud providers (like AWS, Google Cloud Platform, Azure, etc.)", and that you "will most likely run into
RequestBlockedorIpBlockedexceptions when deploying your code to any cloud solutions." - Naive retries make it worse. A
requests.Sessionpools its connection, so a retry through a rotating proxy usually leaves on the same exit IP — the one that was just blocked. You have to rebuild the client between attempts. - A proxy helps but does not guarantee anything. The README says so directly, and there is a visible tail of reports that it did not work for them.
- Some failures no proxy can fix: a proof-of-origin token challenge, and videos that simply have no caption track.
What the two exceptions actually mean
Both derive from CouldNotRetrieveTranscript, and both are raised after YouTube returned something that is not a caption track. There is no documented, stable signal that separates "this IP is banned" from "this particular request looked automated" — the library maps what it can see, and what it can see is a refusal.
Treat them as one condition with one meaning: the request was rejected for looking like automation, and retrying identically will be rejected identically. Anything you do about it has to change something YouTube can observe.
Why it works on your laptop and not on your server
Your home or office connection is a residential IP. It is shared with a handful of humans watching YouTube normally, so its reputation is fine and stays fine. A datacenter IP is the opposite: it belongs to a well-known cloud ASN, it is shared with everyone else on that host, and a large share of the traffic leaving it is automated. YouTube can identify those ranges trivially, and it does.
This is not a hypothesis about the library. It is what the library documents about itself, and it is the single most common issue filed against it — a GitHub issue search for ip blocked in jdepoix/youtube-transcript-api returned 73 results when we checked in August 2026, against a repository with roughly 8,000 stars. Google's own autocomplete for "youtube transcript api" offers "youtube transcript api ip blocked" as a top completion. You are not doing anything unusual.
The practical consequence: a green local test proves nothing about production. If your CI runs on a hosted runner, a passing CI proves nothing either — hosted runners are datacenter IPs too.
Things that look like fixes and are not
- Retrying in a tight loop. Same connection, same exit IP, same answer — plus you have now confirmed to YouTube that something automated is hammering it.
- Sleeping longer between requests. Rate is not the trigger when the IP range itself is the trigger. Slowing down a blocked address gets you blocked more slowly.
- Sending browser headers or a cookie jar. The block does not hinge on your
User-Agent. Copying one from your browser changes nothing you were being judged on. - Giving the box a static IP. This is the one that actively backfires. An Elastic IP, a dedicated egress address, or a static outbound IP makes your traffic more identifiable, not less — you have converted an intermittent block into a permanent one, and you now pay monthly for it. See the Render notes for what that failure looks like day to day.
- Switching cloud regions. A different range with a different reputation buys you days, sometimes hours. It is a reprieve you will spend again.
- Cheap datacenter proxies. You have swapped one datacenter ASN for another, usually one with worse reputation because it is sold by the gigabyte to scrapers.
Things that actually change the outcome
1. Rebuild the client between attempts (free, and most people miss it)
This is the fix with the best effort-to-payoff ratio, and it is invisible until you know to look for it. requests.Session pools the CONNECT tunnel to an HTTP proxy. A rotating gateway assigns an exit IP per connection, not per request — so as long as you reuse the same client object, every retry leaves from the address that was just blocked. Your rotating proxy is not rotating.
Discard the client and build a new one on each attempt, so a new connection is opened:
import random, time
from youtube_transcript_api import YouTubeTranscriptApi
from youtube_transcript_api.proxies import WebshareProxyConfig
from youtube_transcript_api._errors import IpBlocked, RequestBlocked
def fetch(video_id, langs=("en",), attempts=3, base_delay=0.6):
last = None
for i in range(attempts):
# NEW client every attempt -> new connection -> new exit IP.
api = YouTubeTranscriptApi(
proxy_config=WebshareProxyConfig(USERNAME, PASSWORD)
)
try:
return api.fetch(video_id, languages=list(langs))
except (IpBlocked, RequestBlocked) as exc:
last = exc
if i < attempts - 1:
# Jitter matters: a fixed schedule makes concurrent retries fire
# in lockstep, so a rotating gateway hands the whole batch
# adjacent exit IPs and they get blocked together.
time.sleep(base_delay * (i + 1) + random.uniform(0, base_delay))
raise last
Two details in that snippet are the whole point. The client is rebuilt inside the loop, and the backoff carries a random component. A fixed schedule across concurrent workers is how a batch of retries ends up on a contiguous block of exit IPs that get banned as a group.
Note also that youtube-transcript-api documents its client as not thread-safe — one session and cookie jar underneath. If you fan out with threads, build one client per thread rather than sharing one.
2. Route through rotating residential proxies (paid, and honest about it)
This is the library's own recommended answer, and it does work more often than not. It is also the answer the maintainer earns a referral commission on, which is disclosed in the README and is worth knowing when you read it.
What the README also says, and what tends to get skipped: "Be aware that using a proxy doesn't guarantee that you won't be blocked, as YouTube can always block the IP of your proxy!" That is not a disclaimer, it is a description. Several filed issues report residential rotation still failing — for example #511 ("even with webshare"), #549 and #552, all reporting blocks while a rotating residential Webshare config was in place.
Budget for a proxy bill and for it not being sufficient on its own. Combine it with the client rebuild above, or you are paying for rotation you are not receiving.
3. Ask a different door (the fallback most people never build)
YouTube does not have one caption endpoint; it has several client surfaces behind InnerTube, and they are not challenged equally. youtube-transcript-api talks to one of them. yt-dlp can be pointed at others — its player_client extractor argument accepts tv, ios, web and more, and the non-web surfaces are challenged far less aggressively.
In our own production sidecar this is the fallback that clears the largest share of request_blocked walls that a fresh IP could not:
import yt_dlp
opts = {
"quiet": True,
"skip_download": True,
"writesubtitles": True,
"writeautomaticsub": True,
# Different InnerTube surfaces, tried in order.
"extractor_args": {"youtube": {"player_client": ["tv", "ios", "web"]}},
# Don't let format selection raise on a caption-only player response.
"ignore_no_formats_error": True,
}
with yt_dlp.YoutubeDL(opts) as ydl:
info = ydl.extract_info(f"https://www.youtube.com/watch?v={video_id}", download=False)
tracks = info.get("subtitles") or info.get("automatic_captions") or {}
The cost is a second dependency that also breaks when YouTube changes, and yt-dlp ships updates often for exactly that reason. Pin it, and plan to bump the pin.
4. Cache like the transcript is expensive, because it is
A published video's captions do not change. Every refetch is a coin flip against a block for data you already had. Store the transcript keyed by (video_id, language) the first time you get it and never ask twice. This single change removes most of the traffic that gets people blocked, and it is the cheapest thing on this page.
5. Hand the problem to something that already solves it
At some point the honest question is whether transcript retrieval is your product. If it is not, the maintenance is pure overhead: a proxy bill, a retry policy, a fallback client, a cache, and a pager that goes off when YouTube changes something on a Tuesday.
The newer failure no proxy fixes
Since 2026 a second, distinct wall has appeared: YouTube asks the caller to prove it is a real client before it will serve some caption tracks. In the library this surfaces as PoTokenRequired, and it is tracked in the still-open issue #592, where the caption track URL returns an empty body.
This is worth naming separately because it is diagnosed wrong constantly. If you have already bought residential proxies and are still failing on a subset of videos, more IPs will not help — you are being asked for a token, not for a better address. The two failure modes need different responses, and the first step is telling them apart in your logs. Map the exception types individually rather than collapsing everything into one "fetch failed" bucket; you cannot fix what you cannot distinguish.
The failure that is not a block at all
A large share of "the API is broken" reports are videos that genuinely have no captions in any language — NoTranscriptFound or TranscriptsDisabled. No amount of proxy rotation helps, because there is nothing to download. These are permanent outcomes and should never be retried; retrying them is how people burn a proxy budget on videos that will never work.
The only thing that gets text out of a genuinely captionless video is transcribing its audio. That is a different, more expensive operation — see why a transcript can be missing for how to tell the cases apart before you spend anything on them.
A retry policy that reflects all of this
Split your error handling by whether a retry can possibly succeed:
- Retry (transient):
IpBlocked,RequestBlocked, connection errors, timeouts. Rebuild the client, jittered backoff, small attempt count. - Never retry (permanent):
NoTranscriptFound,TranscriptsDisabled,VideoUnavailable,AgeRestricted,InvalidVideoId. Record the outcome and move on. - Escalate, do not retry:
PoTokenRequired. A retry loop here just spends money confirming the same answer.
Note that a dead proxy raises ProxyError via requests.exceptions.ConnectionError, not the builtin ConnectionError — omitting it from your retryable tuple silently skips every retry and drops you into your generic error path. That one caught us.
When a hosted API is the right answer, and when it is overkill
Straight answer, including the cases where you should not pay anyone:
- Overkill: a script on your own machine, a one-off research pull, a notebook you run by hand, anything under a few dozen videos a month from a residential connection. The open-source library is genuinely good and free. Use it.
- Borderline: a small side project on a cheap VPS. Try the client rebuild plus caching first — that is free, and for low volume it is frequently enough.
- Worth paying for: anything with users waiting on the response, anything on serverless, anything where a failed fetch is a support ticket, and anything where the person maintaining the proxy stack is also the person who is supposed to be building the product.
What VidWords does about it
We are not describing a category from the outside — VidWords runs youtube-transcript-api in production and hits every wall on this page. What we built around it:
- Rotating residential egress for every YouTube-bound request.
- A fresh client per attempt, for exactly the pooled-connection reason above, with jittered backoff so concurrent retries do not land on adjacent exit IPs.
- A
yt-dlpcaption fallback that rotates player clients (tv,ios,web) when the primary surface is challenged. - An audio-transcription last resort for videos that have no caption track at all, so a captionless video is a priced outcome rather than a dead end.
- A shared cache, so a video someone else already fetched costs nobody another request against YouTube.
- Distinct error codes per failure mode, returned per video, so a permanent outcome never looks like a transient one.
From your side that is one HTTPS call. Failures come back per video with a machine-readable code rather than sinking the batch:
import requests
resp = requests.post(
"https://vidwords.com/api/transcripts",
headers={"Authorization": f"Basic {API_TOKEN}"},
json={"ids": ["dQw4w9WgXcQ", "9bZkp7q19f0"]},
timeout=60,
)
resp.raise_for_status()
for video in resp.json()["results"]:
if "error" in video:
print(video["id"], "->", video["error"]) # no_transcript, video_unavailable, invalid_id
continue
print(video["title"], video["text"][:120])
Up to 50 video ids per request; one delivered transcript costs one credit, and a video with no transcript costs nothing. A free account includes 25 transcripts a month and works for API calls too, so you can port your integration before deciding anything. Full parameters and response fields are in the API reference, and the API comparison covers the alternatives fairly, including the open-source route.
Platform-specific notes
The shape of the failure differs by where you deploy, and so does the fix:
- AWS Lambda — ephemeral egress from a shared pool, so it fails intermittently and a NAT gateway makes it permanent.
- Render (and Railway, Heroku, Fly) — static outbound ranges shared per region, so once it breaks it stays broken.
- Google Colab — the most heavily used surface for exactly this library, and the hardest place to keep proxy credentials.
FAQ
Will a VPN on my laptop fix it?
If the code runs on your laptop you probably do not have the problem. If you are asking because your server is blocked, a VPN on your laptop changes nothing about the server's egress.
Is scraping transcripts against YouTube's terms?
That is a question for your own counsel, not for a blog post. What we can say factually: the official Data API's captions.download requires OAuth and permission to edit the video, so there is no API-key-only official route to another creator's caption text — which is why this entire category exists. See the developer guide.
Does upgrading the library fix it?
Stay current — it is actively maintained and fixes do land. But the block is a property of your IP, not of your version, and the reports span releases.
How do I tell a block from a captionless video in my logs?
Map every exception class to its own code rather than catching CouldNotRetrieveTranscript broadly. NoTranscriptFound and TranscriptsDisabled both inherit from it, so a broad catch turns a normal content outcome into a fake infrastructure alarm.