yt-dlp Proxy Guide: --proxy, SOCKS5, Sessions and Workers

Written by

in

Pointing yt-dlp at a proxy takes one flag. Getting it to work reliably across thousands of videos and dozens of workers takes more. This guide covers the proxy URL format, SOCKS vs HTTP, environment variables, config files, the Python API, sticky sessions, aria2c, and a troubleshooting table for the errors you'll actually hit.

Everything below was checked against the yt-dlp README and source for release 2026.10.26, the current stable release as of November 2026. yt-dlp changes quickly, so run yt-dlp -U and check yt-dlp --help if a flag behaves differently for you.

TL;DR

  • Use --proxy "http://USER:[email protected]:8080" or --proxy "socks5h://USER:[email protected]:1080". If the password has special characters, URL-encode it.
  • In yt-dlp, socks5:// is silently rewritten to socks5h://, so DNS is resolved by the proxy either way. That's not true of every external downloader.
  • If --proxy is not set, yt-dlp reads HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY from the environment. --proxy "" forces a direct connection.
  • Fetch a video's metadata and download its media from the same exit IP. Use sticky sessions, or one static IP per worker, rather than a new IP on every request.
  • --geo-verification-proxy does nothing for YouTube in the current source. Only --proxy matters.
  • aria2c only supports HTTP proxies, and ffmpeg can't use SOCKS proxies.

HTTP/HTTPS vs SOCKS5, and socks5 vs socks5h

The network options describe --proxy URL as "Use the specified HTTP/HTTPS/SOCKS proxy". The scheme you put in the URL decides the proxy type:

Scheme What it means Notes
http:// Plain HTTP proxy. HTTPS traffic is tunnelled through it with CONNECT The most widely supported option. A URL with no scheme is treated as http://.
https:// TLS connection to the proxy itself Needs the requests or curl_cffi request handler. The plain urllib handler doesn't support it. The README lists requests as the dependency "for HTTPS proxy". Standalone binaries include it.
socks5:// SOCKS5 yt-dlp rewrites it to socks5h (see below).
socks5h:// SOCKS5 with remote DNS The proxy resolves hostnames.
socks4://, socks4a:// SOCKS4 / 4a No username/password support in SOCKS4. Rarely useful today.

Most tools use the h suffix to mean "let the proxy resolve DNS". Without it, the client resolves hostnames locally, which leaks DNS lookups and can pick a CDN node near your server instead of the proxy. yt-dlp handles this differently: in yt_dlp/utils/networking.py, clean_proxies() maps 'socks5': 'socks5h' with the comment "compat: socks5 was treated as socks5h". As a result, both schemes give you remote DNS for yt-dlp's own requests.

Still write socks5h:// explicitly. It documents your intent, and the same URL may later be passed to curl or another tool where socks5:// really does mean local DNS.

HTTP or SOCKS5? For yt-dlp alone, either works. Choose HTTP if you plan to use aria2c or ffmpeg as the downloader (see below). Choose SOCKS5 if your provider only offers SOCKS or you're tunnelling through something like ssh -D.

The --proxy flag and URL format

The general form is scheme://user:password@host:port:

# HTTP proxy with credentials
yt-dlp --proxy "http://USER:[email protected]:8080" "https://www.youtube.com/watch?v=VIDEO_ID"

# SOCKS5 with remote DNS
yt-dlp --proxy "socks5h://USER:[email protected]:1080" "https://www.youtube.com/watch?v=VIDEO_ID"

# Force a direct connection (overrides env vars and config files)
yt-dlp --proxy "" "https://www.youtube.com/watch?v=VIDEO_ID"

URL-encode special characters in passwords

Characters like @ : / # ? % in a username or password break URL parsing. You'll usually get a confusing connection error or a 407 instead of a clear message. Percent-encode the credentials. yt-dlp and its HTTP backends decode them before sending authentication:

python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' 'p@ss:w/rd#1'
# p%40ss%3Aw%2Frd%231

yt-dlp --proxy "http://USER:p%40ss%3Aw%2Frd%[email protected]:8080" URL

Always quote the proxy URL in the shell. A #, & or ! left unquoted will be read by the shell, not passed to yt-dlp.

--geo-verification-proxy

The README says this option makes yt-dlp "use this proxy to verify the IP address for some geo-restricted sites", while --proxy is still used for the actual download. Only extractors that call the geo_verification_headers() helper use it. In 2026.10.26, the YouTube extractor doesn't. For YouTube, --geo-verification-proxy has no effect. If you need a YouTube video to appear to come from a given country, the exit IP from --proxy is what counts.

Environment variables

When --proxy is absent, yt-dlp calls Python's urllib.request.getproxies() (see the proxies property in YoutubeDL.py). In practice:

  • http_proxy/HTTP_PROXY and https_proxy/HTTPS_PROXY are used for those URL schemes. If only the HTTP one is set, yt-dlp reuses it for HTTPS. Set HTTPS_PROXY=__noproxy__ to undo that.
  • all_proxy/ALL_PROXY is the fallback for any scheme.
  • no_proxy/NO_PROXY is honoured.
  • On macOS and Windows, if no proxy environment variables are set, getproxies() can fall back to the system proxy settings. A desktop VPN or proxy app can therefore change yt-dlp's routing without you noticing.
  • Any --proxy value replaces the environment completely, including --proxy "".
export HTTPS_PROXY="http://USER:[email protected]:8080"
export HTTP_PROXY="$HTTPS_PROXY"
yt-dlp URL

Config files

Any CLI option can go into a config file. yt-dlp searches, in order: the file given to --config-locations, a portable yt-dlp.conf next to the binary, yt-dlp.conf in the -P home path (or the current directory), the user config, and the system config. Recommended user locations are ${XDG_CONFIG_HOME}/yt-dlp/config on Linux/macOS and ${APPDATA}/yt-dlp/config on Windows. ~/yt-dlp.conf, ~/.yt-dlp/config and /etc/yt-dlp.conf also work.

# ~/.config/yt-dlp/config
--proxy "http://USER:[email protected]:8080"
--socket-timeout 30
--retries 10
--fragment-retries 10

Two gotchas. Config lines are parsed like a shell, so quote values containing #. And --ignore-config on the command line skips all of these files. That helps when you're debugging which proxy is actually in effect. yt-dlp -v prints which config files were loaded.

Using a proxy from the Python API

The option keys match the CLI flags: proxy, socket_timeout, retries, fragment_retries, source_address. One difference matters a lot: according to the docstrings, retries and fragment_retries default to 0 in the API but 10 in the CLI. Set them explicitly.

import yt_dlp

opts = {
    "proxy": "http://USER:[email protected]:8080",
    "socket_timeout": 30,
    "retries": 10,
    "fragment_retries": 10,
    "outtmpl": "data/%(id)s.%(ext)s",
}

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

Check your exit IP

Before blaming YouTube, confirm the proxy works and see which IP you're actually exiting from. With curl:

curl -x "http://USER:[email protected]:8080" https://api.ipify.org
curl -x "socks5h://USER:[email protected]:1080" https://api.ipify.org

A better check goes through yt-dlp's own networking stack, so the proxy parsing, handlers and environment handling are the same ones your downloads use:

import yt_dlp

with yt_dlp.YoutubeDL({"proxy": "http://USER:[email protected]:8080"}) as ydl:
    print(ydl.urlopen("https://api.ipify.org").read().decode())

Run it twice in a row. If the IP changes between calls, your endpoint rotates per request or per connection. That matters for the next section.

Sticky vs rotating sessions: keep one video on one IP

Downloading a YouTube video takes several requests: the watch page and player API calls (metadata, format list, signed media URLs), then many range requests to googlevideo.com for the media. The yt-dlp FAQ puts it this way: "In many cases, requests for the video (to download/play it) must come from the same IP address and with the same cookies and/or HTTP headers." Google doesn't document YouTube's signed media URLs, so the exact binding rules are an observed behaviour rather than a contract. The practical rule is simple, though: if extraction happens on IP A and the media download on IP B, expect intermittent 403s. The PO Token Guide adds another binding: most PO tokens are tied to a session or a video ID, which is another reason to keep each video's requests together.

Proxies come in two broad modes:

  • Rotating per request or connection: every new TCP connection may exit from a different IP. That works for stateless page fetches and is risky for yt-dlp, because one download opens several connections.
  • Sticky sessions: the provider pins you to one exit IP for a set time, usually selected by a session ID in the proxy username. The exact syntax is provider-specific, so check your provider's docs. Static or dedicated IPs behave like a permanently sticky session.

The pattern that works: one sticky session per video (or per worker, for a batch of videos), long enough to cover extraction and the full media download. Then move on to a new session. Rotation still spreads load across the pool; it just happens between videos, not inside one.

Running many workers, each with its own proxy session

Run a separate YoutubeDL instance per job, in separate processes, and derive a session ID from the video so retries of the same video get the same IP. The username format below is illustrative; substitute your provider's sticky-session syntax.

import hashlib
import urllib.parse
from concurrent.futures import ProcessPoolExecutor

import yt_dlp

PROXY_USER = "USER"
PROXY_PASS = urllib.parse.quote("PASS", safe="")
PROXY_HOST = "proxy.example.com:8080"


def proxy_for(url: str) -> str:
    session = hashlib.sha1(url.encode()).hexdigest()[:12]
    # Sticky-session username syntax varies by provider; this format is illustrative.
    return f"http://{PROXY_USER}-session-{session}:{PROXY_PASS}@{PROXY_HOST}"


def fetch(url: str) -> tuple[str, int]:
    opts = {
        "proxy": proxy_for(url),
        "socket_timeout": 30,
        "retries": 5,
        "fragment_retries": 5,
        "outtmpl": "data/%(id)s.%(ext)s",
        "download_archive": "archive.txt",
        "quiet": True,
        "noprogress": True,
    }
    try:
        with yt_dlp.YoutubeDL(opts) as ydl:
            return url, ydl.download([url])
    except yt_dlp.utils.DownloadError:
        return url, 1


if __name__ == "__main__":
    with open("urls.txt") as f:
        urls = [line.strip() for line in f if line.strip()]
    with ProcessPoolExecutor(max_workers=8) as pool:
        for url, code in pool.map(fetch, urls):
            print(code, url)

With a list of static proxies instead, assign one proxy per worker. From the shell, a file with one URL PROXY pair per line plus xargs does the same job:

xargs -P 8 -L 1 sh -c 'yt-dlp --proxy "$1" -o "data/%(id)s.%(ext)s" -- "$0"' < jobs.txt

The shared archive.txt above is fine for a single machine. Across machines, use a real job queue. Concurrency per IP matters as much as the total: many parallel videos behind one exit IP is how IPs get flagged. Pacing and IP reputation are covered in fixing "Sign in to confirm you're not a bot", and sizing the total pipe in bandwidth for large-scale video downloads.

aria2c as the external downloader

--downloader aria2c hands HTTP/FTP downloads to aria2c. In downloader/external.py, yt-dlp passes your --proxy value to aria2c as --all-proxy. It also sets -x16 -s16 --min-split-size 1M by default, then appends whatever you give --downloader-args.

yt-dlp --proxy "http://USER:[email protected]:8080" \
  --downloader aria2c \
  --downloader-args "aria2c:-x 4 -s 4 --connect-timeout=15 --timeout=30 --max-tries=5" \
  "https://www.youtube.com/watch?v=VIDEO_ID"

Three caveats:

  • HTTP proxies only. The aria2 manual gives the proxy format as [http://][USER:PASSWORD@]HOST[:PORT]. aria2c doesn't speak SOCKS. With a SOCKS proxy, use the native downloader. (ffmpeg has the same limitation: yt-dlp warns that it "does not support SOCKS proxies".)
  • 16 connections per file. On a per-connection rotating proxy, each segment may come from a different IP, which brings back the 403 problem. Use a sticky session and fewer connections (-x 4), or skip aria2c.
  • aria2c reads proxy environment variables itself. Only an explicit --proxy is forwarded by yt-dlp, so test the exact configuration you deploy.

Add -v to see the final command line ([debug] aria2c command line: ...) and confirm which arguments took effect.

Timeouts and retries

Flag (API key) Default What it does
--socket-timeout (socket_timeout) 20 s (DEFAULT_TIMEOUT in the networking layer) How long to wait on an unresponsive connection. Raise it to 30–60 s for slow residential exits.
-R, --retries (retries) 10 CLI / 0 API Retries for the download on expected network errors. Accepts infinite.
--fragment-retries (fragment_retries) 10 CLI / 0 API Retries per fragment for DASH, hlsnative and ISM.
--retry-sleep [TYPE:]EXPR none Backoff between retries, e.g. --retry-sleep exp=1:30 or --retry-sleep fragment:linear=1::2.
--extractor-retries 3 Retries for known extractor errors.

Retries go through the same proxy URL. If that URL is a sticky session, they reuse the same IP, which is what you want for a transient error. For a hard block (repeated 403 or 429), retrying on the same IP rarely helps. Fail fast, record the video, and requeue it on a fresh session.

Choosing a proxy type for this workload

Video collection is unusual among proxy workloads: few requests, lots of bytes. That changes the trade-offs.

Type Strengths Weaknesses for yt-dlp
Datacenter (shared) Cheap, fast, stable IPs Cloud and hosting ranges are the most likely to get bot checks and 403s from YouTube. Shared IPs inherit other users' reputation.
Residential (rotating) IPs look like home users, many locations Usually billed per GB, which gets expensive at video volumes. Bandwidth and latency vary by peer. Sessions can drop mid-download.
ISP / static residential Residential-registered IPs on datacenter hardware. Stable, so naturally sticky Fewer IPs per dollar. A flagged IP stays flagged until replaced.
High-bandwidth dedicated (unmetered, per-Gbps) Predictable cost for terabyte-scale transfer, consistent throughput, capacity reserved for you You pay for the capacity whether you use it or not. IP reputation still depends on the ranges and on how hard you push each IP.

The test that matters is your own: run a few hundred videos through each candidate, then compare 403/bot-check rates and cost per delivered GB.

Troubleshooting

Symptom Likely cause Fix
407 Proxy Authentication Required Wrong credentials, unencoded special characters in the password, or source-IP allow-listing on the provider side URL-encode the credentials. Test with curl -x. Check the provider's allow-list.
Unable to connect to proxy / Connection refused Wrong host or port, wrong scheme (SOCKS port with http://), or a firewall Match the scheme to the port the provider documents. Check with curl -x.
SSL: CERTIFICATE_VERIFY_FAILED or WRONG_VERSION_NUMBER Using https:// for a plain HTTP proxy (or the reverse), or an intercepting proxy Usually you want http://. --no-check-certificates hides the problem instead of fixing it; only use it for debugging.
Timeouts, Read timed out Slow or overloaded exit, peer dropped Raise --socket-timeout, add --retry-sleep, reduce concurrency per IP.
HTTP Error 403 on media after extraction succeeded Extraction and download from different IPs (rotating proxy, aria2c multi-connection), missing PO token, outdated yt-dlp Sticky session per video, yt-dlp -U, see the PO Token Guide.
"Sign in to confirm you're not a bot" IP reputation or request rate See our dedicated guide.
Proxy seems ignored --proxy in a config file, a system or desktop proxy, or NO_PROXY matching yt-dlp -v --ignore-config, then check the exit IP through ydl.urlopen.

A note on compliance

YouTube's Terms of Service restrict automated access and downloading, and a proxy doesn't change that. If you're collecting video for training data, copyright and licensing also matter (for example, filtering for Creative Commons–licensed content), as do privacy obligations for faces and voices (GDPR and similar laws) and a process for honouring takedown and opt-out requests. Get legal review for your jurisdiction and use case.

If you'd rather not run this yourself

Corpora offers dedicated 1–40 Gbps unmetered proxy capacity (HTTP(S) and SOCKS5, yt-dlp compatible, rotating pools, multi-region) for teams running their own stack. See pricing. If you only need the end result, Corpora can also collect, deduplicate and deliver video, audio, subtitles and metadata to your object storage. Get in touch for a free sample batch. For the pipeline side, see building a YouTube video dataset for multimodal training and downloading Panda-70M, HD-VILA and InternVid.

FAQ

How do I use a SOCKS5 proxy with yt-dlp?

Pass --proxy "socks5h://USER:[email protected]:1080". yt-dlp treats socks5:// as socks5h://, so DNS goes through the proxy either way. Don't combine SOCKS with --downloader aria2c or ffmpeg, which don't support SOCKS proxies.

Does yt-dlp use the HTTP_PROXY environment variable?

Yes, when --proxy isn't given. It reads HTTP_PROXY, HTTPS_PROXY, ALL_PROXY and NO_PROXY (upper or lower case) through Python's urllib. Any --proxy value, including --proxy "", overrides them.

Why do I get 403 Forbidden when downloading through a rotating proxy?

Usually because the media requests left from a different IP than the one that fetched the video's metadata and signed URLs. Use a sticky session for the whole video. Also update yt-dlp and check PO-token requirements.

Can I use a different proxy for each video in yt-dlp?

Not within a single CLI invocation. --proxy applies to the whole run. Launch one process (or one YoutubeDL instance) per video or per worker, each with its own proxy URL, as in the worker example above.

How do I put a password with special characters in the yt-dlp proxy URL?

Percent-encode it, e.g. with urllib.parse.quote(password, safe=""), so that @ becomes %40 and : becomes %3A. Then quote the whole URL in the shell.