RequestBlocked and IpBlocked mean YouTube refused the IP address your code runs from, not that the video has no captions. Cloud addresses are refused most of all: on 20 October 2026, youtube-transcript-api 1.2.4 was blocked 9 of 9 times from a cloud server and read 9 of 9 transcripts through a residential exit. Three ways out follow.
The fix depends on where your code runs and how many videos it reads. Below: what each error means in the package's own source, the measurement, and three ways out with Python you can paste, from the cheapest to the one with nothing left to maintain.
What RequestBlocked and IpBlocked mean
Both come from the open-source package, not from an official YouTube API. In version 1.2.4 they are raised in these cases:
| Exception | When version 1.2.4 raises it | What it tells you |
|---|---|---|
RequestBlocked | The player answers LOGIN_REQUIRED with "Sign in to confirm you're not a bot" | YouTube will not serve this address without a signed-in viewer |
IpBlocked | HTTP 429 Too Many Requests, or a reCAPTCHA in the watch page | The address asked too often or is flagged; it is a subclass of RequestBlocked |
TranscriptsDisabled | "Subtitles are disabled for this video" | The video has no captions; not a block |
NoTranscriptFound | None of the languages you asked for exists | Pass the spoken language, for example languages=["it", "en"] |
AgeRestricted | The video needs a signed-in viewer | Cookie sign-in is currently unsupported in the package |
VideoUnavailable | The video was removed or made private | Not a block |
Because IpBlocked is a subclass, except RequestBlocked catches both. Catch IpBlocked first when you want to treat the two differently: a 429 can pass with time, a "not a bot" answer on a cloud address does not.
Why it works on your laptop and fails on the server
The package README says it in one line: "YouTube has started blocking most IPs that are known to belong to cloud providers." Your laptop sits on a home or office line, a residential address. The same code on AWS, Google Cloud, Azure or any VPS runs from a datacenter range. We measured it from our own server in a Frankfurt datacenter (DigitalOcean) on 20 October 2026, with three public videos, three rounds two seconds apart, 17:48 to 17:49 UTC:
| Route | Attempts | Result | Time per attempt |
|---|---|---|---|
| Direct, from the datacenter address | 9 | RequestBlocked 9 of 9 | 0.78 to 1.11 s |
| Same server and code, rotating residential exit in the United States | 9 | 9 of 9 read: 192 Italian snippets, 286 and 338 English ones | 1.57 to 2.61 s, median 2.09 s |
The very first request was refused, so this was not a rate limit: the address itself was the problem. Slowing down would not have helped, changing the address did.
Way 1: run it from a residential connection, and slow down
For a few hundred videos, the simplest way out is to run the job where the address is already residential: your own machine, an office line, a small box at home. Then keep the pace human. When the package raises IpBlocked after a burst, wait and try again with a growing pause; when it raises RequestBlocked, waiting rarely helps.
import random, time
from youtube_transcript_api import YouTubeTranscriptApi, IpBlocked, RequestBlocked
ytt = YouTubeTranscriptApi()
def fetch_politely(video_id, languages=("en",), tries=4):
for attempt in range(tries):
try:
return ytt.fetch(video_id, languages=list(languages))
except IpBlocked:
# HTTP 429 or a captcha: this address asked too often, so wait and retry
time.sleep(15 * 2 ** attempt + random.uniform(0, 5))
except RequestBlocked:
# "confirm you're not a bot": the address is refused, change it (way 2 or 3)
raise
raise RuntimeError(f"{video_id}: still blocked after {tries} tries")
for vid in ["aircAruvnKk", "CDjjaTALI68"]:
t = fetch_politely(vid)
print(vid, t.language_code, len(t), "snippets")
time.sleep(3) # one video every few seconds, never a burst
Limits: it does not move to a server, a laptop that sleeps stops the job, and a home address that asks too often gets flagged as well.
Way 2: a rotating residential proxy configured in the package
On a server, give the package an exit on a residential network. It accepts any HTTP or SOCKS proxy through GenericProxyConfig. One detail the README leaves out: in version 1.2.4 the generic config keeps TCP connections alive and never retries a blocked request, because both settings default to off. With a rotating gateway, a kept-alive connection holds on to the same exit, so a refused address stays refused. Two properties fix that:
import os
from youtube_transcript_api import YouTubeTranscriptApi
from youtube_transcript_api.proxies import GenericProxyConfig
class RotatingResidential(GenericProxyConfig):
@property
def prevent_keeping_connections_alive(self) -> bool:
return True # a new connection per request, so the gateway picks a new exit
@property
def retries_when_blocked(self) -> int:
return 5 # a blocked fetch is retried, each time through a fresh exit
# host:port, username and password come from your QuanticData dashboard;
# the country goes in the username, here the United States
user, pwd, host = os.environ["QD_PROXY_USER"], os.environ["QD_PROXY_PASS"], os.environ["QD_PROXY_HOST"]
proxy = f"http://{user}-country-us:{pwd}@{host}"
ytt = YouTubeTranscriptApi(proxy_config=RotatingResidential(http_url=proxy, https_url=proxy))
t = ytt.fetch("aircAruvnKk", languages=["en"])
print(t.language_code, len(t), "snippets")
The second row of the table above used the plain GenericProxyConfig and read 9 of 9; the two properties keep the same setup working when one exit is refused. In a second run of nine with exactly this code, 4 of the 13 watch-page requests were refused and retried through a new exit, and 9 of 9 transcripts came back. Closing the connection on every request means a new tunnel each time, so count on several seconds per transcript. The cost is bandwidth: the package downloads the full watch page before it asks for the captions, and over those nine reads the proxy meter moved 14.4 MB, about 1.6 MB per transcript with the retries included. On QuanticData residential proxies, the Residential Basic line costs $1.00 per GB, down to $0.80 per GB at 1,000 GB:
| Price per GB | Per transcript | Per 1,000 transcripts |
|---|---|---|
| $1.00 | about $0.0016 | about $1.60 |
| $0.80, at 1,000 GB | about $0.0013 | about $1.28 |
The free trial is 100 MB for 5 days with no card, enough for about 60 transcripts before you pay. Use rotating exits, not static ones: the README warns that YouTube bans static proxies after extended use. Limits: the parser reads an undocumented part of YouTube, so a site change can break it until the maintainer ships a fix, and you still get the transcript only.
Way 3: an API that retries for you
The QuanticData Video API runs the whole path on our side. Each read is tried directly first; when YouTube refuses the address or answers empty captions, it is repeated through a residential exit inside the same call. On 20 October 2026 three videos of 8:23, 18:40 and 12:22 came back in 1.7 to 7.3 seconds, all three through that retry, and a read that still fails is not billed.
import os, requests
r = requests.post("https://api.quanticdata.io/v1/scraper/video",
headers={"Authorization": f"Bearer {os.environ['QD_API_KEY']}"},
json={"url": "https://www.youtube.com/watch?v=aircAruvnKk", "max_frames": 0},
timeout=120)
data = r.json()
if data["type"] != "response":
raise RuntimeError(data["message"]) # stable payload.code, and the read is not billed
video = data["payload"]
print(video["transcript"]["language"], len(video["chapters"]), "chapters")
print(video["meta"]["via_proxy"]) # True when the residential retry answered
print(video["content"][:600]) # Markdown timeline with [m:ss] stamps
There is no proxy to configure and no parser to update. You also get what the package does not return: the chapters the author wrote, the description, and up to 30 frames where the speaker points at the screen (here turned off with max_frames: 0). The same call is the read_video tool of the QuanticData MCP server, for Claude, Cursor and other MCP clients; the full walk-through is Train an agent with the Video API, and every field is in the API reference.
Which way to choose
| Your situation | Way | Why |
|---|---|---|
| A few videos, run from your own computer | 1 | Free, nothing to configure |
| A server job that needs the transcript only, and you keep the code | 2 | 9 of 9 read through a rotating residential exit; about $1.60 per 1,000 at $1.00 per GB |
| An agent or pipeline that needs chapters, frames or quotes with timestamps | 3 | One timeline per call, retry built in, billed only on success |
| Thousands of videos with no proxy or parser to maintain | 3 | The retry and the fixes happen on our side |
For the wider picture, with hosted transcript services and the official YouTube Data API side by side, see YouTube transcript APIs compared.
What not to do
- Do not sign in with cookies to get past the block. The package's own error message warns that YouTube will eventually ban the account used, and cookie sign-in is currently unsupported in version 1.2.4.
- Do not set HTTP_PROXY and hope. In the project's issue tracker the maintainer points users to the package's proxy config instead: pass the proxy with
proxy_config, as in way 2. - Do not hammer a refused address. More requests from a flagged IP keep it flagged; back off on IpBlocked, change the address on RequestBlocked.
- Do not read what you may not read. Stick to public videos and follow YouTube's terms and the law where you run: a proxy changes where a request comes from, not what you are allowed to do.