Automated media library management for *Arr ecosystems. Warden operates in two complementary modes:
- Vigilance — hunts for missing and upgrade-eligible media across your library
- Defence — clears stalled, broken, and problematic downloads from your queue
Run either mode independently, or combine both for full library automation.
docker compose up -d| Mode | Env Value | Description |
|---|---|---|
| Warden | WARDEN_MODE=warden |
Run both Vigilance and Defence (default) |
| Vigilance | WARDEN_MODE=vigilance |
Search only — find and trigger missing/upgrades |
| Defence | WARDEN_MODE=defence |
Cleanup only — remove stalled and broken downloads |
See config.example.yaml for a complete reference. Copy it to config.yaml and customize.
Define your *Arr instances under instances:. Each instance shares the same configuration structure:
instances:
sonarr-instance:
type: sonarr
host: "http://<sonarr-host>:<port>"
api_key: "${SONARR_API_KEY}"
enabled: trueSupported types: sonarr, radarr, lidarr.
Control the granularity of search commands per instance:
| Type | search_type values |
Default | API Command |
|---|---|---|---|
| Sonarr | series, episode |
episode |
SeriesSearch / EpisodeSearch |
| Lidarr | artist, album |
album |
ArtistSearch / AlbumSearch |
| Radarr | movie, collection |
movie |
MoviesSearch / CollectionSearch |
For Sonarr on large libraries, see Per-Tag Search Limits to cap searches per tag and rotate through the backlog.
instances:
sonarr-instance:
type: sonarr
search_type: series # Search entire series instead of individual episodes
lidarr-instance:
type: lidarr
search_type: artist # Search entire artist instead of individual albumsNote: series search uses SeriesSearch with seriesId, which is more efficient than triggering individual episode searches.
Radarr supports grouping movies by collection:
vigilance:
radarr_collection_search_mode: "off" # off | detect | forceoff— Standard movie-level search (default)detect— Group movies by collection when found in wanted endpointsforce— Fetch from/api/v3/collectionand search all monitored collections with missing movies
Configure how Warden hunts for missing and upgrade media:
vigilance: # legacy alias: global
dry_run: false # Log intended searches without sending commands
active_hours: "" # Optional local-time window, e.g. "22:00-06:00"
run_interval_minutes: 30 # How often to check for new items
run_interval_minutes_missing: # Optional missing-only interval in minutes
run_interval_minutes_upgrade: # Optional upgrade-only interval in minutes
missing_batch_size: 25 # Items searched per cycle (0 = disabled, -1 = unlimited)
upgrade_batch_size: 0 # Upgrade searches per cycle (0 = disabled)
search_order: release_date_ascending # alphabetical_* | last_added_* | last_searched_* | release_date_* | random
stagger_interval_seconds: 10 # Delay between individual search commands
retry_interval_days: 5 # Skip items searched within this window
retry_interval_days_missing: 3 # Override for missing items only
retry_interval_days_upgrade: 7 # Override for upgrade items only
fetch_page_size: 2000 # Records per API request
fetch_record_limit: 0 # Cap records pulled per wanted fetch (0 = unlimited)
fetch_timeout_seconds: 120 # HTTP request timeout
max_queue_size: 500 # Pause if queue >= this (0 = disabled)
circuit_breaker_threshold: 3 # Skip instance after N consecutive failures
interleave_instances: false # Alternate between instances in search queue
interleave_types: true # Alternate between missing and upgrade searches
search_after_cleanup: true # Search for replacements after Defence removals
search_after_cleanup_actions: [retry, blocklist]
api_request_interval_seconds: 2 # Min delay between mutating API calls
search_jitter_seconds: 3 # Random extra delay to avoid burst patterns
include_tags: [] # Optional *Arr tag labels to include
exclude_tags: [] # Optional *Arr tag labels to excludeControl when to search for entire seasons instead of individual episodes:
instances:
sonarr-instance:
type: sonarr
season_packs: true # Always use season search
# season_packs: 0.75 # Or: threshold ratio (75% of episodes missing)
# season_packs: 5 # Or: minimum episode countFor very large libraries, paging the entire wanted/missing set every cycle is slow
and can flood your download client. tag_limits instead caps how many series
are searched per *Arr tag each cycle, replacing the whole-library scan:
instances:
sonarr-instance:
type: sonarr
search_type: series
tag_limits:
anime: 10 # up to 10 anime series per cycle
live-action: 10 # up to 10 live-action series per cycle- Each entry triggers one
SeriesSearch, so Sonarr resolves season packs with per-episode fallback (series → season → episode); a whole multi-season series counts as a single unit against the cap. - A per-tag cursor rotates through the backlog across cycles (first N → next N → … → wrap) rather than re-searching the same top-N every time. The cursor is in-memory and resets to the top of each tag on restart.
- Series whose tags are not listed — and untagged series — are not searched by
this path; add their tag to
tag_limitsto include them. - When set,
tag_limitstakes precedence oversearch_typeandseason_packsfor that instance, and the instance is exempt from cross-instance batch allocation (its per-tag caps are authoritative). It is missing-only — upgrades are unaffected. - Raise the numbers to move through a backlog faster. For huge libraries this is
preferable to
fetch_record_limit, which only truncates the scan rather than fairly distributing searches across tags.
Configure how Warden defends your library from problematic downloads:
defence: # legacy aliases: cleanup / killarr
dry_run: false # Log intended removals without deleting queue items
active_hours: "" # Optional local-time window, e.g. "22:00-06:00"
interval: 600 # Run every 10 minutes
batch_size: -1 # Process all stalled items (-1 = unlimited, 0 = disabled)
stagger_interval_seconds: 5 # Delay between removals
circuit_breaker_threshold: 3 # Skip after N consecutive failures
cleanup_page_size: 100 # Queue records per API request
max_cleanup_queue_records: 0 # Cap total records fetched (0 = unlimited)
max_removals_per_instance: 25 # Per-instance removal cap per cycle (0 = no cap)
delete_timeout_seconds: 15 # Timeout for queue deletion calls
fetch_timeout_seconds: 30 # HTTP timeout for queue fetches
retry_interval_minutes: 0 # Cooldown before re-evaluating removed items (0 = off)
removal_order: api_order # api_order | age_ascending | age_descending
cleanup_search_scope: episode # episode | season | series (what ID to search after removal)
protect_downloading_series: false # Hold back stalled items from series with active downloads
queue_max_age_hours: 0 # Clean non-completed items stuck in queue > N hours (0 = off)
interleave_instances: false # Alternate removals between instances
search_after_cleanup: # Optional override for global search_after_cleanup
include_tags: [] # Optional *Arr tag labels to include
exclude_tags: [] # Optional *Arr tag labels to exclude
# Action per stall reason: ignore | remove | retry | blocklist
dangerous_file: blocklist
manual_import: blocklist
no_files: blocklist
no_upgrade: blocklist
stalled: blocklist
missing_items: blocklist
tba_title: blocklist
no_messages: blocklist
download_unavailable: blocklist # Items orphaned by an unavailable download client
unknown: blocklist| Category | Triggered By |
|---|---|
dangerous_file |
Potentially dangerous file extension detected |
manual_import |
Import failures, sample conflicts, matching issues |
no_files |
No eligible video/audio files found |
no_upgrade |
Existing file already meets cutoff or is better |
stalled |
Download stuck (metadata, no peers, locked files) |
missing_items |
Files not found in grabbed release |
tba_title |
Title still "TBA" (unreleased) |
no_messages |
No status messages provided by *Arr |
download_unavailable |
Download client reported unavailable (orphaned queue item) |
unknown |
Unrecognized messages (please report) |
Restrict both modes to a specific local-time window. Set TZ in the container environment to control the timezone used by these checks:
vigilance:
active_hours: "22:00-06:00" # Only run between 10 PM and 6 AM local timeLeave empty or omit for all hours (default).
Skip instances after consecutive failures:
vigilance:
circuit_breaker_threshold: 3 # Vigilance fetch failures
defence:
circuit_breaker_threshold: 3 # Defence cleanup failuresAny vigilance or defence setting can be overridden per instance (legacy global/cleanup/killarr names still accepted):
instances:
lidarr-instance:
type: lidarr
max_removals_per_instance: 5 # Override Defence cap for this instance only
manual_import: remove # Override one cleanup stall category for this instance
search_type: artist
sonarr-instance:
type: sonarr
max_queue_size: 200 # Override Vigilance queue-pause threshold
tag_limits: { anime: 10 } # Per-tag search caps apply per instanceservices:
warden:
image: ghcr.io/johagan94/warden:latest
container_name: warden
restart: unless-stopped
env_file:
- .env
environment:
TZ: Australia/Brisbane
LOG_LEVEL: INFO
WARDEN_MODE: warden
volumes:
- ./config.yaml:/app/config.yaml:ro
networks:
- homelabRegistry: Images are published to GitHub Container Registry (ghcr.io/johagan94/warden). For private deployments, use your own registry URL.
Running Warden alongside decypharr?
The extras/decypharr/ folder has optional host-side
helper scripts for decypharr's FUSE mount lifecycle — a stale-mount watchdog
(heartbeat.sh) and a clean array-stop orchestrator (janitor.sh). They are
not required by Warden and are ignored by anyone not using decypharr. See
extras/decypharr/README.md.
docker build -t warden:latest .MIT