The Setting That Threw Away Every Removal
Why: Damien listed four things in the Movies rdt-client he was tired of cleaning up by hand — month-old finished downloads that never disappear, nameless records reading DIFF_ISSUE: Download already queued, TorBox entries named after a bare hash that download forever with no seeders, and one film stuck at 6/37 downloads failed that never notified anybody. They turned out to be three causes, not four, and the biggest one was a single dropdown.
The Movies instance was holding 94 torrent records. TV was holding 13. Same stack, same back end, same arrs, seven-fold difference — and the reason was one setting that has been quietly discarding Radarr's work for a month.
1. Radarr was doing its job. rdt-client was throwing it away.
Radarr has removeCompletedDownloads=true and removeFailedDownloads=true. It was sending the removal every time. The relevant code is in rdt-client's qBittorrent shim, the path every arr grab takes:
public async Task TorrentsDelete(String hash, Boolean deleteFiles)
{
...
switch (settings.Current.Integrations.Default.FinishedAction)
{
case TorrentFinishedAction.RemoveAllTorrents: ...
case TorrentFinishedAction.None:
logger.LogDebug("Not removing torrents or files");
break;
}
}
One setting decides everything: Settings → qBittorrent / *darr → Finished action. Movies had it on Do nothing. TV had it on Remove torrent from Real-Debrid and Real-Debrid Client. So Sonarr's removals worked and Radarr's were received, logged as "Not removing torrents or files", and dropped. Nothing errored. Nothing warned. The records just piled up — 70 of the 94 were sitting there marked Finished, some from July 25.
There is a trap worth writing down, because we nearly fixed the wrong dropdown. The Provider defaults on the Movies instance were already set to remove-with-a-60-minute-delay, and it had never once fired. It never can:
public async Task<Torrent> TorrentsAddMagnet(String magnetLink, ...)
{
var torrent = new Torrent
{
...
FinishedActionDelay = settings.Current.Integrations.Default.FinishedActionDelay,
FinishedAction = TorrentFinishedAction.None, // hardcoded
...
Every field is read from settings except that one, which is pinned to None. That is why all 94 records read finishedAction: 0 no matter what the UI said. For an arr-added torrent the per-record finished action is always zero, and the Integrations delete path is the only lever that exists.
- Action: set
Integrations:Default:FinishedAction = 1on Movies (TV was already correct). Backups of both settings trees went to/home/plex/ops/rdt-settings-backups/.
2. One dead magnet poisons every download behind it
Two TorBox entries were named after nothing but a hash — 6929b9bd… and 92100cc1…. They were the same release. Radarr grabbed "Charlotte A Du Fun 2018 FRENCH HDRip XviD" twice, five seconds apart at 17:43:37 and 17:43:42, from TorrentDownload via Prowlarr — a DHT-scrape aggregator that serves magnets with an empty &dn= and seed counts that are not real. 29 of the last 54 Radarr grabs came from that one indexer.
A magnet with a dead swarm does not fail on TorBox. It sits in checking with size: -1, zero seeds, zero peers and eta: 8640000 — a hundred days — holding one of the three slots we pay for, for ever. Nothing on TorBox's side times it out.
That is where the nameless records come from, and this is the part that took a while to see. With the slots full, TorBox stops starting new torrents and starts queueing them instead:
dead magnet holds a slot
-> TorBox has no free slot, so it QUEUES the next add
-> rdt-client reads "queued" as a failed add and retries
-> TorBox answers "DIFF_ISSUE: Download already queued."
-> after TorrentRetryAttempts the record parks for ever with no name
-> Radarr shows a nameless item stuck on "qBittorrent is reporting an error"
So symptoms two and three were never separate bugs. Two dead magnets from one bad indexer had produced nine nameless records and ten Unknown Torrent Name entries in a TorBox queue that rdt-client cannot see and nothing was reaping.
- Action: wrote
/home/plex/bin/torbox-slot-guard.py. It evicts a TorBox torrent only when all five hold — active, unfinished, zero progress, zero seeds and zero peers, older than 30 minutes — and separately flushes add-queue entries older than an hour that no rdt-client instance still holds a live record for. Runs four times an hour, because the cost of a jam is every download queued behind it.
3. TorBox's API lies about what it just did
Worth its own heading, because it nearly made the new script useless. The first live run deleted twelve things, every call returned success, and the verify step reported that nothing had changed. Both readings were true: the deletes landed, and torrents/mylist and queued/getqueued were serving a cache that still listed all of them minutes later.
queued/getqueued?type=torrent -> 10 entries
queued/getqueued?type=torrent&bypass_cache=true -> 0 entries
Deletions also return 500 DATABASE_ERROR reasonably often while still succeeding. So the rule for anything talking to TorBox: never read state without bypass_cache=true, and never trust a delete response — only a fresh re-read. Patched into the slot guard and back into torbox-cleanup.py, which had the same blind spot.
4. Thirty-seven files, most of them thirty bytes
"Andrew Schulz Life (2025)" was cached and complete on TorBox and stuck at 6/37 downloads failed with errors, retry count 3/3. The YTS release bundles 37 files, mostly subtitle stubs. MinFileSize was 0, so rdt-client dutifully fetched all 37 individually, TorBox rate-limited the account, and the log filled with hundreds of lines of 429 (Too Many Requests) until it gave up. The account was still showing cooldown_until the following day.
Setting a minimum turns 37 requests into one. Combined with a lifetime and a self-destruct for errored records, three more settings now do work that scripts were being asked to do:
Integrations:Default | TV | Movies | What it buys |
|---|---|---|---|
| FinishedAction | 1 → 3 | 0 → 3 | the arr's removal is actually honoured; 3 (client only) leaves the TorBox copy so a failed import stays recoverable |
| FinishedActionDelay (min) | 0 → 60 | 0 → 60 | an arr can never lose its download-client record mid-import |
| MinFileSize (MB) | 0 → 50 | 0 → 50 | no 429 storm over 30-byte files |
| TorrentLifetime (min) | 0 → 180 | 0 → 180 | a torrent that never starts becomes an error |
| DeleteOnError (min) | 0 → 180 | 0 → 180 | errored records remove themselves |
These apply only to newly added torrents — rdt-client copies them onto each record at add time, so the 94 already stranded carry the old zeroes for ever. Hence the sweep below.
5. Clearing the month that had accumulated
rdtclient-cleanup.py only ever considered records carrying an error, which is why it had been running hourly for a day and reporting Deleted: 0 every time against 94 records — only 10 of them were errored. Extended it with --include-idle, which also sweeps two shapes it previously ignored: records rdt-client reports as Finished, and records stalled with no seeders and no bytes moving. Both need to be older than --idle-age days and absent from every arr queue in a healthy state.
That last qualifier matters. A stalled record sits in Radarr's queue precisely because the download client still holds it, so "an arr mentions it" cannot count as proof of life or the two layers protect each other for ever — the same deadlock the existing --include-arr-referenced flag was added to break. The gate is now "an arr is tracking it with no fault", which correctly protected the five Harry Potter downloads that were genuinely in flight while the run was happening.
- Action: one-time sweep at a three-day threshold cleared 58 stranded records — 49 finished-and-never-removed, 9 stalled with zero seeders, the oldest 34.7 days old. The deletes are record-only (
deleteDataalone); no file on disk and no TorBox entry was touched. - Action: added
--include-idleto the hourly cron run as a permanent backstop, so if the Integrations setting ever gets reverted the leak self-heals within three days instead of growing for a month.
6. The one case that genuinely needs a human
Damien's fourth item was the sharpest: "this is where I assume some kind of manual override SHOULD be needed, yet I get no notifications to do something about it." He was right — nothing watched for it. retryCount >= TorrentRetryAttempts means rdt-client has stopped trying, and until today that state was completely silent.
The first version of the alert fired on 17 records. Sixteen were DIFF_ISSUE residue from the slot jam — things the cleanups resolve on their own and the arr recovers from by re-searching. Seventeen lines to read and sixteen to ignore is how an alert channel gets muted, so the gate was narrowed to two conditions:
- Recoverable — retries exhausted and TorBox still holds the release cached. The bits exist and are retrievable; rdt-client just failed to pull them. Nothing else in the stack will ever try again. This is the real manual-override state.
- Unfamiliar — an error that is not on the known-routine list. An allowlist alone would silently swallow every new failure mode, which is the opposite of what monitoring is for.
Re-run against the same data, that fires on exactly one record: Andrew Schulz, flagged TorBox STILL HAS IT CACHED — retry the download in rdt-client and it should complete. Every alert carries that verdict, because the question a human actually has at 11pm is not "what broke" but "can I fix this or should I let it re-search". It runs twice an hour, comfortably inside the new 180-minute DeleteOnError window, so there is time to act before the record is swept.
7. Where I got ahead of the evidence
Worth recording in full, because the reasoning error is more instructive than the fix. Mid-session Damien reported eight 2160p Harry Potter upgrades that had downloaded but "never moved to their proper final destinations" — Radarr showing them as importPending, nothing in Plex. I went looking, found the files complete on disk, found the rdt-client records gone, and concluded the import had been stranded. It had not been.
Radarr imported all eight itself, in a single 4.3-second batch:
20:40:42.5 Chamber of Secrets <- 0.5s BEFORE my manual command ran
20:40:43.0 Goblet of Fire <- the only one I triggered
20:40:43.6 Order of the Phoenix
...
20:40:46.8 Deathly Hallows Part 1
DownloadedMovieImportService|Processing path appears exactly once per film, all in that batch. Radarr was never retrying and failing; it simply had not tried yet, and when it did it worked first time. The manual scan landed in the middle of a batch already running.
Two specific errors:
- Reading a successful outcome as a failure. The rdt-client records vanishing is what correct operation looks like — the arr removes the download after importing it. I treated the effect of a working import as evidence of a broken one.
- Treating a torn read as a finding. The "proof" that the records had been deleted was a copy of a live SQLite database taken as three separate
docker cpcalls for.db,-waland-shm, while the sweep was actively writing. That is a torn read and it is not evidence of anything.
What is still genuinely unexplained is the 27-minute gap between the downloads completing (20:13–20:24) and Radarr's first import attempt, when its completed-download interval is one minute. That window is exactly when the backfill sweep was running, and the imports fired within two minutes of it being killed. rdt-client's delete endpoint was taking ~32 seconds per call, so an unbounded sweep spent half an hour hammering the same service Radarr polls every minute. Suggestive — but Radarr logged no download-client errors in that window, which argues against the obvious mechanism, so it stays marked unproven.
Two changes came out of it, both worth keeping regardless of whether the sweep was the cause:
- FinishedAction is
3with a 60-minute delay, not1with none. The 2026-08-09 session had already reasoned its way to exactly this —3keeps the provider copy so a failed import is recoverable, and the delay preserves the arr's import window. It applied that reasoning to the wrong profile, and I then discarded the reasoning along with the profile. Wrong twice, in opposite directions. rdtclient-cleanup.pynow defers entirely while any arr is downloading or awaiting import, and caps itself at 20 deletions per run. An hourly janitor has no business running for half an hour against the service the importer depends on, whatever today's cause turns out to have been.
8. What we did not do
The obvious prevention is raising minimumSeeders above 1 in Prowlarr's Standard app profile, which all eight indexers share. We left it alone deliberately: with a debrid back end a cached torrent needs no seeders at all, and a global bump would cost exactly the obscure catalogue titles this library is being filled with. The surgical version — a second app profile with a higher threshold bound only to TorrentDownload — is worth doing if dead magnets keep arriving, but the slot guard now evicts them within half an hour regardless of how they got in, which is the more robust half of the fix.
The older structural gap is unchanged and still the biggest one: nothing enforces cached-only grabbing. The arrs still grab uncached releases and TorBox still fetches them server-side into the three paid slots. The slot guard makes that self-correcting rather than self-inflicted, but a real cache-check layer between Prowlarr and the arrs is still the fix.
Four complaints, three causes, one dropdown. The Movies queue went from 94 records to 3 and TV from 13 to 0, both dead slots came back, and the removal Radarr had been dutifully sending for a month now actually happens.
← Back to Admin Hub