馃帀 Premium Proxies 路 24-Hour Free TrialClaim Now
Guides

Sign In to Confirm You're Not a Bot: Fix yt-dlp on a VPS

yt-dlp says sign in to confirm you're not a bot on your server? Triage it cheapest-first, then fix it with PO tokens, cookies, the right proxy or the API.

S SparkProxy 4 18 min read
Share

Sign in to confirm you're not a bot is the message YouTube returns instead of a player response when it doesn't trust the session asking for a video, and for yt-dlp users it almost always appears the day a job moves from a laptop to a VPS, a cloud instance or a CI runner. The command didn't change. The address, the session state and often the yt-dlp build did. This guide walks the causes from the cheapest to check to the most expensive to fix, shows what PO tokens and cookies change, and says plainly which proxy setups help, which don't, and when the official API is the better tool.

The Exact Message, Old Builds and New

Here is what a current build prints. We reconstructed it from the yt-dlp source at release 2026.10.26 (the latest release on GitHub, published 2026-10-26), where the extractor takes YouTube's playability reason and appends its own cookie hint:

ERROR: [youtube] VIDEO_ID: Sign in to confirm you鈥檙e not a bot. Use --cookies-from-browser or --cookies for the authentication. See  https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp  for how to manually pass cookies. Also see  https://github.com/yt-dlp/yt-dlp/wiki/Extractors#exporting-youtube-cookies  for tips on effectively exporting YouTube cookies

Older builds printed YouTube's text untouched. The canonical report, yt-dlp issue #10128 from June 2024, shows that form:

ERROR: [youtube] 4TWR90KJl84: Sign in to confirm you鈥檙e not a bot. This helps protect our community. Learn more

That difference is your first free diagnostic. The current extractor strips the trailing "This helps protect our community. Learn more" before adding the cookie hint, so if your error still ends in "Learn more", you're running a stale build and nothing else in this guide is worth trying until you update. Note the curly apostrophe in "you鈥檙e": grep for confirm you rather than the full phrase if you match it in logs.

Two neighbouring errors look similar and need different fixes:

MessageWhat it meansFirst move
`Sign in to confirm you鈥檙e not a bot`YouTube refused the session (IP, missing token or no cookies)Work the triage below
`This content isn't available, try again later`Rate limit; yt-dlp says the session "has been rate-limited by YouTube for up to an hour"Wait, then add `-t sleep`
`YouTube is requiring a captcha challenge before playback`A captcha screen in the player responseTreat like the bot wall; cookies plus a cleaner exit
`No supported JavaScript runtime could be found` (warning)yt-dlp can't solve YouTube's JS challengesInstall a runtime, see below

Why Servers Hit the Wall and Laptops Don't

Your laptop reaches YouTube from a consumer ISP address with a long history of ordinary viewing. A VPS reaches it from a hosting network, and hosting networks publish or advertise their address blocks, so classifying them takes no detection work at all. We cover how those range files get consumed in what cloud provider IP ranges are, and how an address accumulates a bad score in what IP reputation is and why it matters. Neither post needs repeating here.

The server also differs in ways that have nothing to do with the IP:

  • No session. A fresh box has no cookies, so every request is an anonymous guest with no viewing history.
  • No token. Some YouTube clients now expect a Proof of Origin token that a real browser generates on its own.
  • Old package. Distro repositories and pinned Docker images often ship a yt-dlp from months ago.
  • No JS runtime. Minimal images rarely include Deno or Node.

Any one of those can produce the same message. That's why "just add a proxy" fails as often as it works: if the cause is a stale build, a residential exit won't save you.

Free trial

Scraping at scale? Skip the blocks.

Fast, unblockable datacentre proxies with unlimited bandwidth.

Triage: Cheapest Check First

Work down this list and stop at the first step that changes the result.

#CheckCostProxy relevant?
1Does the error end in "Learn more"? Update yt-dlpSecondsNo
2Is a JS runtime detected in `-v` output?MinutesNo
3Does the same command work from a home connection?MinutesTells you if the IP is the cause
4Is a PO token provider installed and generating tokens?15 minutesNo
5Do throwaway-account cookies clear it?15 minutes, account riskPartly
6Does a different egress IP clear it?A plan or trialYes
7Do you need video at all, or just metadata?Design changeSometimes removes the problem

Step 3 is the one people skip. Run the identical command, same version, same flags, from a residential connection. On 2026-10-30 we ran yt-dlp 2026.10.26 with Node as the runtime from a consumer ISP connection in India and got metadata back for a public video with no cookies and no token. That's one data point, not a success rate, but it's exactly the control you want: if home works and the server doesn't, the address or its reputation is in play. If both fail, look at your build and flags first.

Update yt-dlp and Give It a JavaScript Runtime

Update through whatever installed it. A pip install pulls the yt-dlp-ejs component with the default extra:

# pip / pipx installs
python3 -m pip install -U "yt-dlp[default]"

# standalone binary
yt-dlp -U

# confirm
yt-dlp --version

The project's README calls yt-dlp-ejs "Required for full YouTube support" and says a JavaScript runtime such as Deno (recommended), Node.js, Bun or QuickJS "is also required to run yt-dlp-ejs." Only Deno is enabled by default. On a box with Node and no Deno, yt-dlp prints this, which we captured on 2026-10-30:

WARNING: [youtube] No supported JavaScript runtime could be found. Only deno is enabled by default; to use another runtime add  --js-runtimes RUNTIME[:PATH]  to your command/config. YouTube extraction without a JS runtime has been deprecated, and some formats may be missing. See  https://github.com/yt-dlp/yt-dlp/wiki/EJS  for details on installing one

Either install Deno or point yt-dlp at Node explicitly:

yt-dlp --js-runtimes node --simulate --print "%(id)s %(title)s" "https://www.youtube.com/watch?v=VIDEO_ID"

The README also notes that without a runtime the web and tv_samsung clients are dropped from the default client list, so a missing runtime shrinks the set of clients yt-dlp can fall back on.

Read the Verbose Log Before Changing Anything

Add -v and read the debug header. These lines come from our own run on 2026-10-30:

[debug] yt-dlp version stable@2026.10.26 from yt-dlp/yt-dlp [c91f8de0b] (pip)
[debug] JS runtimes: node-24.22.0
[debug] [youtube] [pot] PO Token Providers: none
[debug] [youtube] [jsc] JS Challenge Providers: bun (unavailable), deno (unavailable), node, quickjs (unavailable)
[youtube] [jsc:node] Solving JS challenges using node

Three lines answer three triage questions. The version line tells you whether step 1 is done. JS runtimes: tells you whether step 2 is done. PO Token Providers: none tells you that no provider plugin is loaded, so any client that needs a token is running without one.

PO Tokens: What They Fix and What They Don't

The yt-dlp wiki's PO Token Guide (last edited July 12, 2026) defines a Proof of Origin token as "a parameter that YouTube requires to be sent with requests from some clients," and warns that requests without one "may return HTTP Error 403, or result in your account or IP address being blocked." Per that guide, web, mweb, tv_simply, web_music and web_creator need a token for video server (GVS) requests, while tv, web_embedded and android_vr don't. Tokens are bound either to the session (visitor ID or account session ID) or to the video ID, so you can't paste one token into a batch job and forget about it.

You don't generate them by hand. The guide lists two provider plugins; the first is bgutil-ytdlp-pot-provider, which the guide notes is maintained by a yt-dlp maintainer. Its latest release is 2.0.2, published 2026-10-07, and that release is also its most recent commit, so it's actively shipped. Setup is a local HTTP server plus a plugin:

# 1. token server, bound to loopback only
docker run --name bgutil-provider -d --init \
  -p 127.0.0.1:4416:4416 brainicism/bgutil-ytdlp-pot-provider

# 2. plugin, into the same Python environment as yt-dlp
python3 -m pip install -U bgutil-ytdlp-pot-provider

# 3. verify it loaded AND generated a token
yt-dlp -v --simulate "https://www.youtube.com/watch?v=VIDEO_ID" 2>&1 | grep -i "pot"

The plugin's README separates two checks. A PO Token Providers: bgutil:http-2.0.2 (external) line only proves the plugin loaded. You also need a line like Generating a gvs PO Token for web client via bgutil HTTP server. If providers are listed and no "Generating" line appears, the README says the chosen clients "failed earlier (e.g. with LOGIN_REQUIRED) before reaching a step that needs a PO Token," and suggests trying --extractor-args "youtube:player-client=mweb,tv,web_safari".

Then the sentence that should shape your whole plan. From the same README: "a PO Token does not bypass IP-based login restrictions. If you still get 'Sign in to confirm you're not a bot' after a token was generated (common on datacenter IPs), you additionally need to pass cookies." A token proves the request came from a plausible client. It doesn't make a flagged address trustworthy.

Passing Cookies Without Burning the Account

Cookies turn an anonymous guest into a signed-in session, which is what the error text literally asks for. The yt-dlp Extractors wiki explains why a naive export dies within hours: "YouTube rotates account cookies frequently on open YouTube browser tabs as a security measure." Its procedure avoids that:

  1. Open a new private or incognito window and sign in to YouTube.
  2. In that same tab, go to https://www.youtube.com/robots.txt. It should be the only private tab open.
  3. Export the youtube.com cookies to a Netscape-format cookies.txt, then close the window so the session is never opened in a browser again.

The wiki says not to use --cookies-from-browser for this, because it reads your regular profile, not the private session. Copy the file to the server and pass it with --cookies /srv/yt/cookies.txt. For general cookie and session mechanics outside YouTube, see our guide to handling cookies and sessions in web scraping.

The cost is real. The wiki states: "By using your account with yt-dlp, you run the risk of it being banned (temporarily or permanently)." Use a throwaway account, never one tied to anything you care about. The same page gives the project's own rough ceilings: guest sessions around 300 videos per hour (about 1,000 webpage or player requests), accounts around 2,000 videos per hour (about 4,000 requests), and it recommends 5 to 10 seconds between downloads using -t sleep. The README expands that preset to --sleep-subtitles 5 --sleep-requests 0.75 --sleep-interval 10 --max-sleep-interval 20. These are the project's guidance, not figures we measured.

Where a Proxy Helps, and Where It Can't

Be clear about what you're buying. A proxy changes exactly one thing: the source address YouTube sees. Map that against the causes:

CauseCan a proxy fix it?
Stale yt-dlp buildNo
Missing JS runtime or `yt-dlp-ejs`No
Client needs a PO token and none is generatedNo
Rotated or expired cookiesNo
Account flagged for volumeNo; a new IP on a flagged account stays flagged
Server's own IP flagged or on a hosting rangeYes, if the new exit is one YouTube trusts more
Per-IP rate limit on a large batchYes, by spreading requests across exits

The honest caveat for datacenter proxies, ours included: SparkProxy's proxy plans exit from datacenter address space. That moves you off your VPS provider's specific range and away from its neighbours' reputation, and it gives you many addresses to spread a batch across. It doesn't turn the traffic into home traffic. The bgutil README's own note that the bot wall persists after a token "common on datacenter IPs" applies to any datacenter exit. So treat a datacenter proxy as a fix for rate limits and for one badly burned server address, not as a guaranteed cure, and test it during the trial before you plan capacity around it.

One more pairing rule. If you pass account cookies, don't also rotate the exit on every request. A signed-in session that appears from a new address on each call is an odd pattern for any account, and it costs you nothing to avoid. Use a held exit for cookie runs and per-request rotation only for anonymous metadata work. That's our operating practice, not a documented YouTube rule.

Routing yt-dlp Through SparkProxy

yt-dlp takes one proxy for the whole run with --proxy, which accepts HTTP, HTTPS and SOCKS URLs. SparkProxy's gateway is gateway.sparkproxy.io: port 11000 for Random Rotation (a new exit per connection) and port 11002 for Sticky Sessions, which hold the same exit for 5 minutes. Both accept user:pass auth or an IP whitelist.

# anonymous metadata batch: rotate exits, keep the sleep preset
yt-dlp --proxy "http://USER:PASS@gateway.sparkproxy.io:11000" \
  -t sleep --skip-download --write-info-json \
  -a ids.txt -o "/srv/yt/%(id)s.%(ext)s"

# signed-in run: hold one exit, pass the private-window cookies
yt-dlp --proxy "http://USER:PASS@gateway.sparkproxy.io:11002" \
  --cookies /srv/yt/cookies.txt -t sleep \
  "https://www.youtube.com/watch?v=VIDEO_ID"

A Sticky Session holds for 5 minutes, so a long cookie job will change exit every few minutes. Split signed-in work into short batches rather than one overnight run.

If you drive yt-dlp from Python, the same options map onto YoutubeDL parameters. js_runtimes, cookiefile, sleep_interval_requests and extractor_args are all documented keys in YoutubeDL.py:

import os
from yt_dlp import YoutubeDL

PROXY_USER = os.environ["SPARK_USER"]
PROXY_PASS = os.environ["SPARK_PASS"]

opts = {
    "proxy": f"http://{PROXY_USER}:{PROXY_PASS}@gateway.sparkproxy.io:11002",
    "cookiefile": "/srv/yt/cookies.txt",
    "js_runtimes": {"node": {}},
    "sleep_interval_requests": 0.75,
    "sleep_interval": 10,
    "max_sleep_interval": 20,
    "skip_download": True,
    "extractor_args": {
        "youtube": {"player_client": ["mweb", "tv", "web_safari"]},
        "youtubepot-bgutilhttp": {"base_url": ["http://127.0.0.1:4416"]},
    },
}

with YoutubeDL(opts) as ydl:
    info = ydl.extract_info("https://www.youtube.com/watch?v=VIDEO_ID", download=False)
    print(info["id"], info["title"], info.get("duration"))

Does the token server need its own proxy setting? Not in current builds: the yt-dlp source passes the request's proxy to the provider along with the token request, so the token is generated for the route you're actually using. The bgutil README adds one trap: a Docker container's network is isolated, so a proxy running on the host's localhost "will not be accessible from within the container unless you pass --net=host." A remote gateway like ours isn't affected.

Cost works differently from per-GB residential plans. SparkProxy plans start from $75/month (USD, as listed on sparkproxy.io in October 2026) with unlimited bandwidth, so a batch that downloads full video files costs the same as one that only reads metadata. The billing unit is the plan, and there's a 24-hour free trial with no credit card, which is the right place to run triage step 6 against your own target videos.

Only Need Metadata? Skip the Downloader

A lot of jobs that hit this wall never needed the video. If you want titles, durations, view counts or channel data, the official YouTube Data API v3 has no bot wall and no IP question. Google's getting-started page (last updated 2026-10-15) gives projects "10,000 units per day combined" by default and says a read that retrieves a list of resources "usually costs 1 unit."

import os
import requests

r = requests.get(
    "https://www.googleapis.com/youtube/v3/videos",
    params={
        "part": "snippet,contentDetails,statistics",
        "id": "VIDEO_ID_1,VIDEO_ID_2",
        "key": os.environ["YT_API_KEY"],
    },
    timeout=15,
)
r.raise_for_status()
for item in r.json().get("items", []):
    print(item["id"], item["snippet"]["title"], item["contentDetails"]["duration"])

When you need something the API doesn't model, such as the page a given country actually sees, fetch the watch page instead and check its playability status before you parse anything. Our YouTube scraping guide covers field extraction; the part that matters here is detecting the wall. With the SparkProxy Scraping API, json_response=true returns an envelope whose body is base64, so decode it first. proxy_type=premium selects the pool our docs describe as "real ISP IPs," listed at 10 credits without JS rendering (docs read 2026-10-30), and render_js=false keeps it a plain fetch:

import base64
import os
import re
import requests

API = "https://scrape.sparkproxy.io/api/v1"


def watch_page_status(video_id: str) -> tuple[str, str]:
    r = requests.get(
        API,
        headers={"X-API-Key": os.environ["SPARK_API_KEY"]},
        params={
            "url": f"https://www.youtube.com/watch?v={video_id}",
            "render_js": "false",
            "proxy_type": "premium",
            "json_response": "true",
        },
        timeout=90,
    )
    r.raise_for_status()
    envelope = r.json()
    html = base64.b64decode(envelope["body"]).decode("utf-8", "replace")
    m = re.search(r'"playabilityStatus":\{"status":"([A-Z_]+)"', html)
    status = m.group(1) if m else "UNKNOWN"
    if status == "LOGIN_REQUIRED" or "confirm you" in html:
        return status, "bot wall: do not parse, retry later or change route"
    return status, f"ok, credits_used={envelope.get('credits_used')}"


print(watch_page_status("VIDEO_ID"))

Matching on playabilityStatus is sturdier than matching the English sentence, which changes with locale and apostrophe style. We tried to measure how often the premium pool clears the wall on 2026-10-30 and the API returned Cloudflare 521 for every request, so we publish no success rate for it. Test it on your own video list.

Frequently asked questions

FAQ

Your home connection is a consumer ISP address with ordinary history; the VPS sits on a hosting network that YouTube can classify instantly, and it usually also lacks cookies, a PO token provider and sometimes a current yt-dlp build. Run the same command from both places to separate the IP from the setup.

Often not. The bgutil plugin's README says a PO token "does not bypass IP-based login restrictions" and that on datacenter IPs you usually need cookies as well. Install a provider anyway, since some clients fail without one.

Not with an account you care about. The yt-dlp wiki warns that using your account "you run the risk of it being banned (temporarily or permanently)." Use a throwaway account, export from a private window as the wiki describes, and keep request rates low with -t sleep.

Sometimes, not reliably. A datacenter proxy moves you off a burned server address and spreads rate limits across many exits, but it is still datacenter space. Expect it to help with throttling more than with a hard bot wall, and test during a trial.

Use Random Rotation on port 11000 for anonymous metadata batches and Sticky Sessions on port 11002 when you pass account cookies, so one signed-in session doesn't appear from a new address on every request. Sticky Sessions hold for 5 minutes, so keep cookie batches short.

Whenever you only need metadata such as titles, durations, statistics or channel details. The API has no bot wall, a default of 10,000 quota units per day, and most read calls cost 1 unit, which is cheaper and steadier than fighting the downloader.

Special Discount 路 20% off

Get 20% off your first month

Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.

Save up to 15% more on quarterly, half-yearly and yearly plans

Claim Discount

About the Author

SparkProxy Technical Team. SparkProxy runs datacenter proxy infrastructure across 80+ countries and a managed Scraping API for teams collecting public web data. This guide was checked against yt-dlp 2026.10.26, bgutil-ytdlp-pot-provider 2.0.2, the yt-dlp wiki pages linked above and Google's YouTube Data API documentation, all read on 2026-10-30, with command output captured on our own test machine the same day.

Sources: yt-dlp README 路 PO Token Guide 路 Extractors wiki 路 bgutil-ytdlp-pot-provider 路 YouTube Data API getting started 路 SparkProxy Scraping API docs

Keep reading

Related articles

SSL WRONG_VERSION_NUMBER With a Proxy: The Fix

Getting SSL WRONG_VERSION_NUMBER through a proxy in Python? It's almost always https:// in the proxy URL. The one-character fix, plus every SSLEOFError cause.

SparkProxy路Guides