2026-05-16 Media Stack Download Pipeline Recovery

What I set out to do

Noticed a few stuck downloads in qBittorrent. Wanted to understand why and clear them. Turned into a much wider session that uncovered a force-resume cascade breaking auto-cleanup, dead public-tracker swarms for older content, and ultimately the root cause behind all of it: port 6881 was never reachable from WAN, so every torrent that had to rely on inbound connections was crippled.

What I actually did

Worked through the stack live via API. No commits — everything was runtime state changes against Sonarr / Prowlarr / qBittorrent / the router. Two follow-up project notes capture what needs to be encoded back into source-of-truth.

  1. Initial diagnosis: 6 stuck Industry S01 torrents + 1 stuck AoT S04E01. All forcedDL, 0 KB/s, 0-25% progress. All sourced from 1337x re-uploads of NTb releases, all in dead swarms (trackers reported 5-7 seeds, qBittorrent connected to 1-3, none serving). Bulk-deleted + blocklisted via Sonarr’s /api/v3/queue/bulk. Sonarr’s autoRedownloadFailed immediately re-grabbed the same releases under slightly different filenames — blocklist matches title strings, not release groups or indexers. Second pass used skipRedownload=true to stop the loop.

  2. MHA S00E23 importPending. Torrent display name said “S00E23 More” but VARYG had renamed the file inside the torrent to S08E147 (their absolute-numbering scheme, but MHA only has 7 seasons + specials). Sonarr’s filesystem parser saw S08E147, found no such episode, rejected as “Invalid season or episode.” Used POST /api/v3/command {name:ManualImport} to bind the file to episodeId 102. Replaced the existing ToonsHub/NF rip with the VARYG/CR dual-audio release (CF score 306 vs whatever ToonsHub was — Anime Dual Audio kicker).

  3. Deprioritized 1337x in Prowlarr via API. PUT each indexer with new priority: Nyaa.si=15, EZTV=20, 1337x=40. Triggered POST /api/v1/command {name:ApplicationIndexerSync,forceSync:true} to push to Sonarr/Radarr. Confirmed not persistentscripts/setup-prowlarr.sh:92,112 hardcodes .priority = 25 on the JSON sent at indexer creation, and the script’s add_indexer short-circuits when the indexer already exists (scripts/setup-prowlarr.sh:67-69). The early-return is technically a bug per AGENTS.md (“define the desired state and always apply it”) but works in our favor here — API changes survive docker compose up. They don’t survive down -v or a future fix to that early-return. Captured in Persist Prowlarr Indexer Priorities in setup-prowlarr.sh for a separate worktree.

  4. Plex was missing several Industry S01 episodes I didn’t realize I’d broken. Cleanup nuked 6, only 3 were imported beforehand (E05, E06, E08). Manually grabbed CAKES (E01, E04, E07) and GGWP (E02, E03) via POST /api/v3/release after removing the matching blocklist entries. All but E03 grabbed cleanly. E03 GGWP had a particularly dead swarm — single reported seed across all working trackers.

  5. 18 torrents stuck in forcedUP cluttering the list. Caused by manual Force Resume clicks (user confirmed). force_start: True overrides max_ratio=0 + ShareLimitAction=Stop, so Sonarr’s removeCompletedDownloads never sees the pause-on-complete signal that gates cleanup. Wrote qBittorrent Force Resume Bypasses Share Limits as an atomic note. Fixed with POST /api/v2/torrents/setForceStart value=false on all 18 hashes. Torrents transitioned forcedUP → stoppedUP, Sonarr’s RefreshMonitoredDownloads swept them within seconds. List shrank 20 → 2.

  6. AoT S04E01 was “queue-blocked” by a dead Funimation metaDL torrent. Every alternative was rejected with “Release in queue already meets cutoff: HDTV-1080p v1” because Sonarr’s release-decision logic treats a queued-but-stuck torrent as “we already have a candidate.” Removed the Funimation queue item with blocklist=false (might be viable later, just not now), re-searched, grabbed [SubsPlease] Shingeki no Kyojin (The Final Season) - 60 (1080p) from Nyaa.si. Same pattern repeated for Industry S01E03 — the GGWP queue blocker prevented re-grabbing the NTb release we’d unblocklisted.

  7. Bumped queue caps from 3 to 20. User asked. max_active_downloads, max_active_torrents, max_active_uploads all set to 20. Observed no speed gain — adding 13 more peer connections to AoT S04E17 took it from 41 KB/s to 48 KB/s. Confirmed the limit was per-torrent swarm health, not slot count.

  8. Libtorrent thread tuning. async_io_threads=10 → 32, hashing_threads=1 → 8, memory_working_set_limit=512 → 2048, file_pool_size=100 → 500. Container has 8 cores, host has 16. Tuned to ~4× container cores per libtorrent’s recommendation. Helps recheck/import throughput; won’t help network-bound speeds.

  9. Found the actual root cause. Used canyouseeme.org POST-form probe from the host (the only way to actually test inbound from WAN — qBittorrent’s connection_status: connected is a self-report based on tracker chatter, not an external probe). Result: port 6881 NOT reachable from outside. This explained the entire 6-month pattern of “trackers report N seeds, qBittorrent connects to 2.” Without inbound, qBittorrent can only initiate outbound; every peer behind their own NAT (residential, mobile, etc.) is unreachable.

  10. macOS firewall was already disabled (socketfilterfw --getglobalstate = 0). Mac was listening on *:6881 (Docker port mapping intact). Router gateway at 10.0.0.1 — Rogers Ignite gateway, identified by the Server: Xfinity Broadband Router Server header. The 24ms ping to the LAN gateway is bizarre (typical LAN is <3ms) — suggests Wi-Fi mesh or bufferbloat on this Mac specifically; not relevant to the port issue but worth noting.

  11. SSDP M-SEARCH from the host found the router’s UPnP IGD at http://10.0.0.1:49152/IGDdevicedesc_brlan0.xml. The Rogers gateway runs Linux/5.4.201-prod-23.2-231009, UPnP/1.0. qBittorrent has upnp: True in preferences but its UPnP requests never reach the router — Docker bridge networks don’t forward SSDP multicast (239.255.255.250), so qBittorrent can’t discover the IGD from inside the container. This is the structural reason port-forward never auto-configured itself despite both ends supporting UPnP.

  12. Added the port mapping via SOAP from the host. Parsed the IGD description, found the WANIPConnection control URL, POSTed AddPortMapping for both TCP and UDP. Both returned HTTP 200. Verified with GetGenericPortMappingEntry enumeration — entries [1] (TCP) and [2] (UDP) for 6881 → 10.0.0.46:6881, both enabled. Re-tested with canyouseeme.org: “Success: I can see your service on 99.234.104.89 on port (6881).”

  13. Effect was immediate. AoT S04E17 jumped from conn=2 to conn=17 after a tracker reannounce. AoT S04E01 (SubsPlease - 60) went from queuedDL 0 KB/s to 3.9 MB/s downloading. After grabbing the previously-stalled releases, Industry S01E03 NTb (unblocklisted, then re-grabbed) connected to 13 peers immediately and started downloading at ~60 KB/s — a release that had been completely dead during the first cleanup pass.

  14. End state: AoT S04 30/30, Industry S01 7/8 (E03 in flight), MHA S00E23 upgraded to dual-audio, qBittorrent list down to ≤2 active torrents at any time, port forward live on the router, Prowlarr priorities active.

What was striking

  • The “queue blocker” pattern repeated three times. Each time, Sonarr was unable to grab better alternatives because its release-decision logic treats whatever’s in the queue (even a 0% stuck torrent) as “we already have a candidate of this quality.” The first stuck Industry NTb torrents blocked Sonarr’s auto-redownload from finding CAKES/GGWP. The Funimation S04E01 metaDL blocked grabbing SubsPlease. The GGWP S01E03 stall blocked re-grabbing NTb. Removing the queue item is the precondition for any alternative grab to succeed, not just a cleanup step. Worth noting somewhere durable that “stuck queue item ≠ inert; it actively suppresses alternatives.”

  • Force Resume is a footgun in a managed pipeline. It’s a UI affordance for “just keep this going no matter what,” but in a Sonarr+qBittorrent setup the entire cleanup automation hangs on torrents reaching a paused/stopped state. Clicking Force Resume on a few torrents to “fix” a slow download silently disables removeCompletedDownloads for those torrents forever. The 18 zombie seeders in the list were the receipts. Captured in qBittorrent Force Resume Bypasses Share Limits.

  • The fact that connection_status: connected is misleading. qBittorrent shows this when it can ping a tracker — not when it’s actually reachable from WAN. The only way to actually know is to probe from outside the LAN. Documented this so future-me doesn’t trust the self-report. Months of “slow downloads” probably traced to no-inbound-port; the symptom was masked by qBittorrent claiming it was fine.

  • UPnP-in-Docker doesn’t work for the reason I expected and didn’t expect. I assumed UPnP was off on the router. It wasn’t — the router happily responds to SSDP from the host. The blocker is that Docker’s bridge network silently drops multicast (239.255.255.250 SSDP discovery) before it can reach the LAN. qBittorrent’s UPnP module sends discovery requests that never get a reply. This is a fully solved problem (use network_mode: host, or host.docker.internal, or run a UPnP relay) but no one in the linuxserver/qbittorrent docs mentions it. The path of least resistance is what I did today: bypass the container’s UPnP, add the mapping from the host via SOAP. Captured in Persist qBittorrent UPnP Port Mapping in Media Stack for the work that needs to land in main.

  • The seedbox 37.48.71.178 from Worldstream (NL) was the actual hero for the Industry CAKES/GGWP grabs. Same IP appeared on multiple torrents at 2-3 MB/s each. Without that one box in the swarm, even the “healthy” public-tracker releases would have been slow. Public-tracker BitTorrent for older content is essentially “are any commercial seedboxes still serving this release” — there’s almost no organic residential seeding anymore for 2020-era TV.

Top 3 tomorrow

  1. Spin a worktree off main for Persist Prowlarr Indexer Priorities in setup-prowlarr.sh — add a priority argument to add_indexer(), set the three overrides (Nyaa.si=15, EZTV=20, 1337x=40), and fix the early-return bug at scripts/setup-prowlarr.sh:67-69 while there. Local test via docker compose up -d orchestrator.

  2. Spin a worktree for Persist qBittorrent UPnP Port Mapping in Media Stack. Decide between the three approaches (network_mode: host vs UPnP renewal cron vs document manual setup) and implement. The UPnP mapping I added today survives until the router reboots; without a renewal mechanism we lose it on Rogers’ ~quarterly firmware push.

  3. Make a real decision about Usenet. Newshosting + NZBGeek is ~$15/mo total for the kind of reliability that today’s debugging session was symptomatic of. Public trackers for 2020-era TV are a losing fight no matter how much I tune Prowlarr.