yt-dlp HTTP 403: Forbidden — the Subtitle and Unranged-GET Triggers¶
Problem¶
A download that worked yesterday fails at the video step:
[info] Writing video subtitles to: .../Some Video.en.vtt
[download] 100% of 1.29MiB in 00:00:00 at 3.37MiB/s
ERROR: unable to download video data: HTTP Error 403: Forbidden
The subtitle file downloads fine. The video never starts, or dies partway. Retrying does not help.
This 403 looks identical to two better-known ones — the missing-PO-token 403 and the stale-.part resume 403 — and it is neither. If you have already updated yt-dlp, confirmed the PO token provider is generating tokens, and cleared every .part file, and it still fails, this page is the one you want.
Two independent triggers¶
They are separate faults that produce the same error string. A given video may hit one, the other, or both.
Trigger 1 — Subtitles invalidate the media URL¶
With --write-auto-subs or --embed-subs active, yt-dlp fetches the timedtext subtitle file before the video. That request invalidates the authorization on the media URL that was just issued, so the video fetch is rejected immediately at 0 bytes.
Reproduced on a single video:
| Subtitles | Runs | Result |
|---|---|---|
| Enabled | 4 | 403 at 0 bytes, every time |
| Disabled | 3 | Completed, every time |
Forcing a different player client does not help — player_client=default still 403s, and player_client=tv cannot see the format at all.
Trigger 2 — The single unranged full-file GET¶
Without chunking, yt-dlp issues one large GET for the whole file. YouTube increasingly rejects that mid-stream on large formats, even with subtitles fully disabled. A second, unrelated video died twice at ~20 MB this way.
--http-chunk-size 10M forces Range: requests instead, which YouTube serves without complaint. The same video then completed at 288 MB, exit 0, zero 403s.
Diagnosis¶
Work down this list. Stop at the first one that matches.
- Is there a
.partfile?find <output-dir> -name "*.part*". If yes, and the line above the error reads[download] Resuming download at byte N, it is the resume trap — clear it and rerun. See the PO-token runbook. - Is the PO token provider generating? Run with
-vand look forRetrieved a gvs PO Token. If that line is present, the token is not your problem. - Does it fail at 0 bytes with a
.vttwritten just above the error? → Trigger 1. Retry with--no-write-auto-subs --no-embed-subs. - Does it fail partway through a large file? → Trigger 2. Retry with
--http-chunk-size 10M.
Fix — a two-pass wrapper¶
The combination that survived both triggers (3/3 across two videos) is chunked download with subtitles suppressed, followed by a separate subtitle pass:
#!/bin/bash
# ytdl — two-pass yt-dlp wrapper
set -o pipefail
yt-dlp --http-chunk-size 10M --no-write-auto-subs --no-embed-subs "$@" \
&& yt-dlp --skip-download --write-auto-subs "$@"
Drop it at ~/.local/bin/ytdl, chmod +x, and use ytdl <url> in place of yt-dlp <url>.
Trade-off: subtitles arrive as a .vtt sidecar next to the video rather than embedded in the container. For a Plex library this costs nothing — Plex reads sidecar subtitles natively, as long as the basename matches. If you specifically need them muxed in, mux them yourself with ffmpeg after both passes finish.
Leaving the flags in ~/.config/yt-dlp/config instead of a wrapper does not work, because the two passes need opposite subtitle settings.
Do not validate this fix with --test¶
--test downloads only the first 10 KiB — and it does so via a small Range: request. That means it exercises exactly the code path that already works, and it can never reproduce an unranged-GET 403.
Four --test runs "passed" during this investigation and the 403 was briefly declared resolved. The next real download failed identically. Validate with an actual download, or at minimum watch it get past the byte offset where it previously died.
The same warning applies to --simulate, which never fetches media at all.
Not the cause¶
Things ruled out, so you do not spend time re-ruling them out:
- Version drift. The affected box ran
2026.07.04, which was the latest GitHub release at the time. "Update yt-dlp first" had nothing to do. - PO token provider. Tokens were being generated and retrieved successfully on every failing run.
- Stale
.partfiles. None existed;--no-continuewas already active. - Player client. Neither
defaultnortvroutes around it. - Throttling. It reproduces on demand with stable counts, not intermittently.
A note on the bgutil HTTP provider¶
If your log shows this on every run:
…it is usually noise, not your 403. yt-dlp falls back to the bgutil deno script and generates tokens fine. Chasing the :4416 daemon because it is the loudest line in the output is a well-worn dead end. Confirm token generation with -v first; if tokens are being retrieved, the daemon is irrelevant to the failure in front of you.
Related¶
- yt-dlp HTTP 429: Too Many Requests — different error, also often hits on the subtitle step
- yt-dlp Merge Fails: 'Invalid data found when processing input' (Stale Cache)
- yt-dlp YouTube JS Challenge Fix on Fedora
- yt-dlp: Video Downloading