Skip to content

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.

  1. Is there a .part file? 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.
  2. Is the PO token provider generating? Run with -v and look for Retrieved a gvs PO Token. If that line is present, the token is not your problem.
  3. Does it fail at 0 bytes with a .vtt written just above the error? → Trigger 1. Retry with --no-write-auto-subs --no-embed-subs.
  4. 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 .part files. None existed; --no-continue was already active.
  • Player client. Neither default nor tv routes 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:

WARNING: [youtube] [pot:bgutil:http] Error reaching GET http://127.0.0.1:4416/ping

…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.