yt-dlp HTTP 429: Too Many Requests (YouTube Rate Limiting)¶
Problem¶
yt-dlp fails with a 429 — most often on the subtitle step, sometimes on video formats:
Subtitles are usually the first thing to break because YouTube's timedtext (subtitle) endpoint has its own, much stricter per-IP rate bucket than the video/audio CDN. A download can fetch the full video fine and then 429 only on the .vtt.
There are two distinct causes, and the error output tells you which one you have.
Diagnosis: which case is this?¶
Look for this warning immediately before the 429:
WARNING: The extractor specified to use impersonation for this download,
but no impersonate target is available.
- Warning present → Case 1: broken impersonation. yt-dlp is being fingerprinted as a bot and rate-limited aggressively.
- No warning, 429 on subtitles only → Case 2: hot
timedtextbucket. Impersonation is fine; you've simply made too many subtitle requests from this IP recently.
Confirm impersonation health directly:
Real entries (Chrome-133 Macos-15 curl_cffi) = impersonation working. An (unavailable) table = Case 1.
Case 1: Broken Impersonation (curl_cffi)¶
Cause: yt-dlp needs curl_cffi to impersonate a real browser's TLS fingerprint. Without it (or with an unsupported version), YouTube detects the non-browser client and rate-limits with 429s.
Fix (pin curl_cffi to the supported range):
⚠️ Do not run a bare
pip install -U curl_cffi. As of yt-dlp 2026.03.17, the backend inyt_dlp/networking/_curlcffi.pyhard-caps at0.14.x:Installing
curl_cffi 0.15.0silently disables impersonation —yt-dlp --list-impersonate-targetswill show every source as(unavailable)even thoughimport curl_cffiworks fine. Always pin to<0.15until yt-dlp widens the range.
Verify:
Should now show real entries, not the (unavailable) table.
Case 2: Hot timedtext Bucket (subtitles only)¶
Cause: the subtitle endpoint's per-IP quota is already exhausted — typically from a batch of downloads or from retrying a failed download several times (each retry re-hits timedtext). Impersonation is healthy; the video/audio path is unaffected.
Check the auto-vs-manual split first
Auto-translated captions and manually-authored subtitle tracks are rate-limited separately, and the auto path is far more aggressive. A video can 429 forever on its auto track while its manual track downloads instantly. Waiting does not help in that case — you are waiting on a bucket that was never the problem. Always run the zero-cost check below before deciding to wait.
Fix — try these in order:
# 0. FIRST -- identify which track is actually blocked. --list-subs fetches the
# player response only, NOT timedtext, so it costs zero requests against the bucket.
yt-dlp --list-subs --skip-download https://www.youtube.com/watch?v=VIDEO_ID
# "Available subtitles:" -> manually-authored tracks (e.g. en-US)
# "available automatic captions" -> auto/auto-translated (e.g. en-en-US)
#
# If a manual track exists, ask for it explicitly. --write-auto-subs alone will
# take the auto path even when a real track is sitting right there:
yt-dlp --skip-download --write-subs --no-write-auto-subs \
--sub-langs "en-US" https://www.youtube.com/watch?v=VIDEO_ID
# 1. Only if the manual track is also blocked (or does not exist):
# wait 15-60 min and retry. NOTE: not reliable -- see occurrence #2, where a
# 34-minute cooldown changed nothing because the auto track was the blocked one.
# 2. Retry now, but throttle subtitle requests:
yt-dlp --sleep-subtitles 5 https://www.youtube.com/watch?v=VIDEO_ID
# 3. Skip subtitles for this download (finishes cleanly if the video already downloaded):
yt-dlp --no-write-subs --no-write-auto-subs https://www.youtube.com/watch?v=VIDEO_ID
Reproducing the auto-vs-manual split¶
A controlled A/B on a single video — same IP, same client, minutes apart
(majorhome, 2026-08-22, M_G_f5g47wI):
URL="https://www.youtube.com/watch?v=VIDEO_ID"
# Which tracks exist? Costs ZERO timedtext requests.
yt-dlp --list-subs --skip-download "$URL"
# "Available subtitles:" en-US <- manually authored
# "available automatic captions:" en-en-US, es-en-US, ... <- auto / auto-translated
# A -- manual track
yt-dlp --skip-download --write-subs --no-write-auto-subs \
--sub-langs "en-US" -o "/tmp/subtest/%(title)s.%(ext)s" "$URL"
# [info] Downloading subtitles: en-US
# [download] 100% of 23.49KiB <- SUCCESS
# B -- auto track, ~5 minutes later, same IP, same session
yt-dlp --skip-download --write-subs --write-auto-subs \
--sub-langs "es-en-US" -o "/tmp/subtest/%(title)s.%(ext)s" "$URL"
# [info] Downloading subtitles: es-en-US <- selection is correct
# ERROR: HTTP Error 429: Too Many Requests <- YouTube refuses the auto path
Two things fall out of B:
- The per-IP bucket was not exhausted. If it had been, A would have failed too — it ran minutes earlier against the same endpoint family from the same address and returned instantly. Only the auto path is limited.
--write-subs --write-auto-subsstill selects auto tracks correctly. B pickedes-en-USand only then hit the 429, so adding--write-subsdoes not break videos that have auto captions and nothing else. It changes which track is preferred, not which are reachable.
What this does not fix
On a video with only auto captions, there is no manual track to fall back to, so it will still 429 whenever the auto bucket is hot. Preferring manual tracks narrows the failure surface; it does not remove it.
If you batch-download regularly, make the throttle permanent in ~/.config/yt-dlp/config:
✅ Done on majorhome 2026-08-22 —
--sleep-subtitles 5is now permanent in~/.config/yt-dlp/config(backup:config.bak.2026-08-22). Theytdltwo-pass wrapper makes this mandatory rather than optional on that box: pass 2 (--skip-download --write-auto-subs) hitstimedtexton every invocation, so even non-batch use drains the bucket as soon as a video is retried.
Occurrences¶
#1 — majorhome, 2026-07-21: a single-video download fetched formats 137+140 successfully, then 429'd only on the en-en-US subtitle. --list-impersonate-targets showed healthy targets and curl_cffi 0.14.0 was pinned correctly — classic Case 2. Waiting and retrying resolved it.
#2 — majorhome, 2026-08-22: same signature, second occurrence, which promotes the
throttle from a suggestion to the default (rule of two). ytdl was run twice within
~2 min on one video (M_G_f5g47wI); pass 1 completed the 182 MB mp4, pass 2 429'd on
en-en-US and left no .vtt sidecar. Impersonation healthy (Chrome-133/136 targets,
curl_cffi 0.14.0 pinned).
The documented Case 2 remedy did not work here, and that is the lesson. A 34-minute
cooldown plus --sleep-subtitles 5 produced an identical 429 on the second attempt. What
actually resolved it: --list-subs (zero timedtext cost) showed the video had a manual
en-US track in addition to the auto-translated en-en-US the wrapper was requesting.
Asking for en-US explicitly downloaded 23.5 KiB / 308 cues instantly, with no 429,
while the auto track was still blocked. The per-IP bucket was never exhausted — only the
auto-caption path was limited. The mp4 never needed re-fetching, since Plex reads the
.vtt sidecar natively.
Practical consequence: --write-auto-subs on its own is the wrong default. It takes
the auto path even when a better, less-throttled manual track exists. Prefer requesting
both (--write-subs --write-auto-subs), which downloads the manual track when present and
falls back to auto only when it is not.
⚠️ Noise to ignore in that output:
WARNING: [youtube] [pot:bgutil:http] Error reaching GET http://127.0.0.1:4416/ping. yt-dlp probes the HTTP PO-token provider first, fails, and falls back toscript-deno, which succeeds on the very next line. The warning is unrelated to the 429 either way.SUPERSEDED 2026-08-28 — the "by design" half of this note was wrong. It used to read "majorhome uses the bgutil script provider by design — there is no HTTP server, container, or systemd unit for it." Nothing was listening on 4416 because the server had simply never been deployed, not because script mode was chosen. A
bgutil-potsystemd user service now runs the matching server (tag 1.3.2, prebuiltserver/build/main.js) from the clone already at~/bgutil-ytdlp-pot-provider, so a healthy majorhome run now logs[pot:bgutil:http] Generating a gvs PO Token ... via bgutil HTTP serverand no warning at all. If you see the 4416 warning on majorhome today, it is a real fault — checksystemctl --user status bgutil-pot. See the vault runbookyt-dlp-po-token-majorhome(MajorInfrastructure/Runbooks — vault-only, not published to this wiki).
#3 — majorhome, 2026-08-28: third occurrence, and the first where neither documented
remedy applies. ytdl on hJwkm64TYxY completed pass 1 with "has already been
downloaded" and then 429'd on track en. The occurrence #2 fix — prefer the manual
track — has nothing to prefer here: --list-subs reports hJwkm64TYxY has no subtitles,
only automatic captions. Auto-only videos have no fallback, and waiting is already known
to be unreliable on the auto bucket (#2). On a video like this, the honest answer is
that the sidecar may simply not be obtainable while the bucket is hot. The mp4 is
unaffected and plays fine; Plex just has no captions for it.
The useful finding was structural, not about this video. The ytdl wrapper chained its
two passes with a bare &&, so pass 2 fired on every invocation — including ones where
pass 1 did nothing. Re-running ytdl on an already-complete download printed "has
already been downloaded", exited 0, and still sent a timedtext request for a sidecar
that in the normal case was already on disk. Every retry of a finished download spent a
request against the tightest bucket YouTube gives us, which is precisely the drain the
--sleep-subtitles note above was trying to slow down.
Fixed the same day: pass 1 now reports its final media path via
--print-to-file after_move:filepath, and pass 2 is skipped when every file it names
already has a .vtt/.srt sidecar. after_move:filepath is populated even on the
"already downloaded" branch (verified on this video), and unlike capturing stdout it
leaves the progress display on a real tty. Re-running a complete download now prints
ytdl: subtitle sidecar already present — skipping subtitle pass and exits without
touching the network.
Also cleared out in the same pass: --write-auto-subs and --embed-subs were still
sitting in ~/.config/yt-dlp/config even though the wrapper negates both. They were
inert for ytdl but re-armed 403 trigger #1 for any bare yt-dlp call, so both were
removed (config.bak-2026-08-28). Checked first that nothing else depended on them —
yt-dlp-watchdog.sh runs --ignore-config and yt-dlp-hevc-convert.sh is an --exec
hook that never invokes yt-dlp.
Confound ruled out: the bgutil HTTP server was deployed during this same session, and the 429 reproduced identically after the
4416warning disappeared. PO-token provider health and thetimedtextbucket are independent — do not read a fixed bgutil warning as a fixed 429, or vice versa.
Related¶
- yt-dlp: Video Downloading — install, config, and standard usage
- yt-dlp YouTube JS Challenge Fix (Fedora) — JS runtime / EJS solver errors (
No supported JavaScript runtime, n-challenge failures). A staleyt-dlp-ejsproduces warnings, not 429s — if you see both, fix impersonation first.