Docs
The deep-reference page — every setting, the recommended way to run a big translation job, how the engine cascade actually behaves, and what to check when something goes wrong. For getting started, see Install and Engine Setup.
Recommended workflow
Getting a library fully translated efficiently is a multi-pass process, not a single run — trying to do everything in one pass either wastes local GPU/CPU time on what's usually a temporary cloud issue, or leaves recoverable items sitting as permanent failures.
1. Make sure Bazarr can see your existing subtitles
A subtitle muxed into the video file itself (not a separate .srt on disk) is invisible to Subtitlarr until Bazarr extracts it — Subtitlarr only ever reads/writes through Bazarr's API, never the filesystem directly. In Bazarr:
- Settings → Subtitles → (disable) "Treat Embedded Subtitles as Downloaded"
- Settings → Providers → add "Embedded Subtitles" (if it isn't already there), so Bazarr actually extracts those tracks to real files instead of just counting them as already satisfied
2–4. Translate in passes, escalate to local last
Run the cascade with local engines fenced off
Put a separator right after your cloud/free-tier instances (see Recommended cascade shape) and let it translate everything it can. Items that hit a genuine content block with no non-Gemini fallback in reach — or any other failure — end up failed and stay there. Leave them alone for now rather than chasing each one down individually.
Once the queue is drained, re-run just the failures
On the Queue page, filter to the Failed chip and run that filtered batch. A second pass over just the failures often clears a meaningful chunk of them on its own — a rate limit expires, a transient error doesn't repeat, quota resets overnight. Targeting the Failed filter specifically means only those items are touched, not a full re-run of everything already done.
failed against the same cloud engine is usually hitting a genuine content block, not a transient error — repeatedly re-submitting the same flagged content risks that provider's own abuse/ToS enforcement against the account behind the key, not just another failed item. If a batch is content you expect to keep tripping the filter, don't loop it against the same key indefinitely — use a throwaway/secondary account for that key rather than one tied to anything you'd mind losing, and consider routing genuinely repeat-blocked content to a local engine instead (see step 4).
Only then, move your local engine above the separator
Drag it up in the Translation Engine page for whatever's still failed. This is deliberate, not automatic: with a non-Gemini engine actually in reach, content-blocked batches get bisected instead of failing outright — Gemini keeps handling everything except the specific isolated cues that trip its filter, and only that small leftover chunk goes to the local engine. Doing this from the start would mean paying local inference time on every single content-blocked item; doing it last means only the genuine leftovers ever reach it.
5. Run the Language Check job
Even a "successful" translation can silently come back in the wrong language — the LLM echoing the source text instead of translating it. Subtitlarr's own structural checks can't catch this, since the response is well-formed, just wrong. The Jobs page's Language Check audits recently-completed items against their actual detected output language and resets any mismatch back to pending for a real retry.
LANGUAGE_CHECK_CRON — otherwise it's manual-only, so remember to run it (or schedule it) after a big batch, not just once and forget it.
How the cascade works
Subtitlarr doesn't use a single fixed engine — you build an ordered list of one or more configured instances (Ollama, llama.cpp, Gemini, NVIDIA NIM, OpenRouter, Groq). Every item is tried against the first instance; if that fails (rate limit, error, content block), it falls through to the next instance in order, and so on. A separator row can fence off a group so nothing below it is ever tried as an automatic fallback — the run simply stops with the remaining items left pending once everything above the separator is exhausted.
Content-block bisection
Gemini's own safety filter (PROHIBITED_CONTENT/SAFETY) can reject a translation batch outright — most likely on R-rated or intense material. Rather than treating the whole batch as a failure and handing all of it to a weaker fallback engine, Subtitlarr bisects the blocked batch and retries each half against the same Gemini instance, so only the actual offending cues end up isolated — the rest of the batch stays on Gemini at full quality.
Splitting stops once an isolated chunk shrinks to 10 cues, or once the item's bisection budget (extra requests spent narrowing down blocks, capped per item) runs out — whichever comes first. Either way, only that small leftover chunk falls back, and it skips straight past every other configured Gemini instance (they'd just trip the same filter again) to the first non-Gemini engine in the cascade, re-chunked to that engine's own configured batch size rather than arriving oversized.
Net effect: a normal batch costs exactly one request, same as always. A batch with a handful of blocked lines costs a few extra Gemini requests to isolate them, and only those lines go to the fallback engine. A batch blocked densely throughout (e.g. an R-rated film flagged every few lines) hits the per-item budget quickly and the remainder falls back in bulk, re-chunked properly — so one heavily-flagged movie can't consume a disproportionate share of your daily Gemini quota chasing a lost cause.
Rate limits & cooldown
3 consecutive failures against an engine instance trip a 24-hour cooldown — the instance is skipped without even attempting a live round-trip for the rest of that window. This clears automatically after 24 hours, or immediately via a Test Connection on that instance, or the Jobs page's manual "clear all rate limits" action.
All settings reference
None of this needs to be touched at container start — every one of these is also editable from the web UI afterward (Settings / Language Rules / Bazarr Connection pages). This table exists purely as a reference for what each setting does and its default. Translation engine setup lives entirely in the Translation Engine page — see Engine Setup — not in this table.
| Variable | Purpose | Default |
|---|---|---|
BAZARR_BASE_URL | Bazarr root URL | set from the UI |
BAZARR_API_KEY | Bazarr API key | set from the UI |
SCHEDULE_CRON | 5-field cron expression for the main scheduled translation job | 10 3 * * * |
AGE_THRESHOLD_DAYS | Days a subtitle must be missing before a scheduled run will translate it | 14 |
DAILY_TRANSLATION_LIMIT | Max items translated per day by scheduled/full runs (0 = unlimited); per-item re-runs bypass this | 100 |
PAUSE_BETWEEN_ITEMS_SECONDS | Rest between translations so the GPU isn't pegged non-stop | 30 |
QUEUE_UPLOADS_ENABLED | Hold translated subtitles locally instead of uploading immediately — see below | false |
PUSH_UPLOADS_CRON | Optional cron to auto-push queued uploads (only meaningful with QUEUE_UPLOADS_ENABLED); blank = manual only | 15 5 * * * |
SYNC_MEDIA_CRON | Optional cron to auto-refresh Bazarr's wanted list; blank = manual only | 0 3 * * * |
SYNC_SUBS_CRON | Optional cron to auto pre-fetch source subtitle content; blank = manual only | 5 3 * * * |
LANGUAGE_CHECK_CRON | Optional cron to audit recently-completed items for the wrong output language and reset any mismatch to pending; needs a check engine picked on the Jobs page first; blank = manual only | 0 5 * * * |
BACKUP_CRON | Daily snapshot of the whole database to /data/backups/; blank disables it | 30 2 * * * |
BACKUP_KEEP_COUNT | How many daily/manual snapshots to retain before pruning the oldest | 20 |
DB_PATH | SQLite file path inside the container | /data/subtitlarr.db |
RUN_CONCURRENCY | Reserved for future use | 1 |
LOG_LEVEL | Logging verbosity | INFO |
PORT is accepted but currently has no effect — the container always listens on 7777 internally (map it to any host port you like via Docker's own port mapping).
Queue uploads, explained
If your Bazarr host — or its storage, e.g. a NAS array — spins its disks down when idle, every immediate upload wakes it. With QUEUE_UPLOADS_ENABLED=false (the default), a completed translation uploads to Bazarr right away; over a long run translating many items, that's one wake-up per item.
Setting it true holds each finished translation locally as translated_pending_upload instead, and nothing touches Bazarr's storage until you push the whole batch at once — manually from the Jobs page, or automatically via PUSH_UPLOADS_CRON. That turns N wake-ups into one.
| Your setup | Recommended |
|---|---|
| Disks/storage never spin down | Either setting works — false is simplest |
| Disks spin down, but are already kept awake by something else | false — no benefit to queuing |
| Disks spin down and nothing else keeps them awake | true — push the batch once you're ready, or schedule it |
Troubleshooting
Translations come back mostly untranslated, or with low cue recovery
This is usually a batch-size problem, not a translation-quality one. The batch token budget (set per engine instance) is not the same thing as that model's context window — a batch that technically fits inside the context window doesn't mean a smaller/weaker local model can reliably format that much structured output. Small models lose structural reliability on long responses well before hitting their real context limit. Try lowering the batch token budget on that engine instance before assuming anything else is wrong.
An item is stuck as "translating" after a restart
There's no checkpointing mid-item — if the app process is killed while an item is actively translating, it's automatically reset back to pending the next time the app starts, and gets fully retried later. Nothing is lost, but whatever GPU/API time was already spent on that attempt is.
A run stopped early with items still pending
Check the engine cascade — this is expected behavior when every instance above a separator becomes rate-limited or disabled mid-run, not a bug. The run stops rather than silently falling through to local engines fenced off behind a separator. Pick up the leftovers with a manual run, or wait for cooldowns/quota to reset.
A completed subtitle is in the wrong language
Run the Language Check job — this exact scenario (a well-formed response that's simply still in the source language) is what it exists to catch, since structural checks alone can't detect it.
Known limitations
- Doesn't download missing subtitles. Subtitlarr only translates a subtitle that already exists in some language — finding/downloading subtitles from providers is Bazarr's job, not this project's. An item with no existing subtitle in any language is skipped, not sourced; that needs speech-to-text, which is out of scope here.
- Doesn't re-time or fix subtitles with bad sync/timing issues. Translation is text-only — the original timestamps are always reused as-is; a subtitle that's out of sync before translation stays out of sync after.
- Doesn't come with a pre-configured LLM or API key. You bring your own engine — a local Ollama/llama.cpp server, or your own API key for Gemini/NVIDIA/OpenRouter/Groq. Nothing is bundled or pre-authorized.
- Subtitlarr never mounts your media library directly — everything goes through Bazarr's REST API. If Bazarr can't see a subtitle, neither can Subtitlarr.