Skip to content

Settings — Scheduler

The Scheduler page is where every recurring job in Sublarr lives. It’s the operations console: see when each job last ran, when it’ll next fire, whether it succeeded, and force a run-now or pause without restarting the container. Backed by APScheduler with a SQLAlchemy job store, so jobs survive restarts with their next-fire-time intact.

The page shows one row per registered job. Seventeen jobs ship out of the box. Several take their interval from a setting elsewhere, so your numbers can differ from the defaults below — the page always shows what is actually scheduled.

Library and queue

JobDefault triggerEffect
wanted_scannerevery 6 hWalks the library and queues searches for items missing a target language. Interval follows Settings → Automation.
wanted_searchevery 4 hProcesses the wanted queue — this is the one that talks to providers. Interval follows Settings → Automation.
subtitle_automationevery 2 minDrains the subtitle-automation queue — extracting embedded tracks and translating local sidecars. By far the most frequent job; see Subtitle Automation.
standalone_scanevery 6 hScans standalone library sources that have no *arr behind them.
upgrade_scanevery 24 hLooks for better replacements for subtitles you already have.

Maintenance

JobDefault triggerEffect
cleanupnightly 03:45Runs the cleanup rules: orphan delete, format upgrades, trash prune.
foreign_track_sweepevery 6 hWorks through the batched foreign-track removal; see Cleanup. Does nothing unless that rule is enabled.
subtitle_health_sweepnightly 04:30Checks stored subtitles for damage; see Subtitle Health.
dubtitle_scannightly 04:15Detects dubtitles; see Dubtitle Detection.
repair_resumeevery 10 minPicks up an interrupted subtitle repair where it left off.
scheduler_history_cleanupnightly 03:15Prunes scheduler history past the configured retention.
translation_events_cleanupnightly 03:30Prunes recorded translation events past their retention.

Data and reporting

JobDefault triggerEffect
anidb_syncevery 7 daysRefreshes the offline AniDB dump.
stats_rollupnightly 04:00Aggregates the daily statistics the Statistics page reads.
mt_reseekevery 24 hRe-checks machine-translated subtitles for a real one having appeared since.
provider_degradation_checkevery hourWatches for a provider that quietly stopped working; see Providers. Off unless you enable the alerts.
usage_stats_pingevery 24 hSends the anonymous usage ping, if you consented. See Usage Statistics.

Plugins can register their own jobs; those appear in the list with a plugin:<name>: prefix on the job ID.

ColumnEffect
Job IDUnique identifier. Human-readable for built-in jobs.
TriggerCurrent trigger expression — interval (every 6h) or cron (0 3 * * *). Edit inline.
Last runTimestamp + outcome — see Outcomes.
Next runWhen the next firing is scheduled.
Runtime (avg)Median wall-clock time over the last 30 runs.
Run nowTriggers the job out-of-band. Respects per-job concurrency lock.
PauseSuspends future firings without removing the job.
Edit triggerOpen the trigger editor — interval or cron.
HistoryDrawer with the last 100 runs and their outcomes.
Reset to defaultRestore the trigger to the shipped default. Useful if a custom edit is causing issues.
TypeEditor fieldExample
Intervalhours / minutes / secondsevery 6h (every 6 hours)
Cronminute, hour, day, month, day-of-week0 3 * * * (3:00 AM daily)

Cron is preferred for “fire at a specific time of day” jobs (cleanup, anidb_sync). Interval is preferred for “fire every X” jobs (scanners, queue drains).

Clicking Run now:

  1. Queues an immediate firing — bypasses the next scheduled time.
  2. Acquires the per-job lock — if a scheduled run is currently in-flight, the run-now waits.
  3. Records the run as manual: true in history so you can distinguish from scheduled firings.

If a manual run-now collides with a scheduled tick, the second-arriving fire is recorded as skipped_overlap and not run twice.

Pausing keeps the job in the registry but skips firings until resumed. Useful for:

ScenarioAction
Maintenance window — don’t run the scannerPause wanted_scanner until done.
Provider rate-limit incidentPause everything that calls providers.
Investigating a misbehaving jobPause to observe state without further triggering.

Paused jobs show a paused badge; their Next run is replaced with paused.

The History drawer shows recent runs with full timing breakdown:

ColumnEffect
Started atWhen the run began.
Finished atWhen the run ended (or running if in-flight).
DurationWall-clock time.
OutcomeSee Outcomes.
Items processedCount of meaningful work units (varies per job).
ErrorTruncated stack trace on failure; click for full.

Per-job retention is configured via the scheduler_history_cleanup job (default 30 days).

OutcomeMeaning
okThe run finished.
errorThe run raised. The error column carries the reason.
skipped_overlapA previous run of the same job was still in flight, so this firing was dropped rather than run twice.
timeoutThe job exceeded its time budget, was asked to stop, and did stop at its next check point.
timeout_abandonedThe job exceeded its budget, was asked to stop, and was still running when the scheduler gave up waiting.
timeout_not_startedThe job never ran. Every scheduler worker was busy for its whole allowance, so the firing waited in the queue and was dropped without executing a line.

timeout_not_started is not a problem with the job named in the row — that job did nothing at all. It means the scheduler’s worker pool was saturated, which in practice is caused by other jobs that overran and kept running: an abandoned run holds its worker for as long as its work continues. So a burst of timeout_not_started rows usually points at whichever job has been logging timeout_abandoned, not at the ones reporting it.

The remaining outcomes are worth separating. A background thread cannot be killed safely, so cancellation is cooperative: long jobs check between work units and stop there. A timeout therefore means the work really ended. A timeout_abandoned means it did not — the run is no longer being waited on, but the work may still be going, may still be writing files, and may still be holding the job’s lock when the next firing arrives.

If you see timeout_abandoned repeatedly on the same job, the useful question is not how long it was given but where it stopped checking. Cancellation can only take effect at a check point, so a job whose remaining work has none will overrun any budget you give it — raising the number then only delays the same entry. The job’s own log lines around the timeout show what it was still doing, and that is what identifies the missing check point.

Each job’s allowance is sized to fit one whole unit of its work, since that is the granularity at which it can stop at all. A run that legitimately needs longer than one unit to wind down is recorded as timeout, not timeout_abandoned.

A run that was interrupted by the container stopping is recorded as InterruptedByShutdown — expected after an update, not a fault.

Sublarr is designed to run as a single primary instance. Running multiple replicas with the same database would cause every job to fire on every replica.

Set SUBLARR_SCHEDULER_ROLE=disabled on all replicas except the primary to prevent duplicate firings. The primary defaults to primary.

ValueBehaviour
primary (default)Scheduler runs.
disabledScheduler is suspended; replica only serves API/UI traffic.
anything elseStartup raises an error.

Plugins or operators with API access can register custom jobs via POST /api/v1/scheduler/jobs. Custom jobs require a Python callable resolvable at the configured module path; see Plugin Development for the registration pattern.

The header shows aggregate health:

TileMeaning
Total jobsRegistered jobs (built-in + plugin).
ActiveNot paused, not in-flight.
RunningCurrently firing.
Failed (last 24h)Jobs with at least one failure in the last day.

Three or more failures in a row on the same job triggers the scheduler_job_failed notification event.