← All guides

RequestBlocked / IpBlocked in youtube-transcript-api — what actually fixes it

Written by the VidWords Team · · Updated · Report a correction

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

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

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:

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:

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:

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:

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.

Read the API docs →