Resources & Workers¶
fetchly runs downloads, transcodes, and audio analysis concurrently inside a single process, bounded by a resource governor that sizes itself to the host.
Why a single process¶
WORKERS must stay at 1. The job queue and the SSE subscriber registry live in process memory with no cross-process coordination:
- A second Gunicorn worker would process the same job twice
- Clients would subscribe for events to a process that never sees their job
This is a correctness constraint, not a throughput trade-off, and the container entrypoint enforces it. Parallelism comes from worker threads and semaphores inside the one process.
CPU detection¶
The governor detects the effective CPU allocation, not just nproc. It is cgroup-aware, so a container limited to --cpus=1.5 is sized for 1.5 CPUs rather than for the host's core count.
Auto-sizing¶
With every variable left at its default:
| Limit | Formula | Bounds |
|---|---|---|
| Worker threads | ceil(cpus × 1.5) | 1–4 |
| Queue depth | workers × 2 | — |
| CPU semaphore | ceil(cpus) | ≥ 1 |
| Analysis semaphore | min(2, ceil(cpus)) | ≥ 1 |
| I/O semaphore | ceil(cpus × 4) | ≥ 2 |
| Transcode semaphore | min(2, ceil(cpus)) | ≥ 1 |
| Parallel fragments | ceil(cpus × 1.5), capped by free MB ÷ 64 | 2–8 |
The shapes reflect the workloads: downloads are I/O-bound and get a generous limit, while transcoding and BPM analysis are CPU- and memory-hungry and are held to at most two at a time.
Worked example¶
| Effective CPUs | Workers | Queue | CPU | Analysis | I/O | Transcode |
|---|---|---|---|---|---|---|
| 0.5 | 1 | 2 | 1 | 1 | 2 | 1 |
| 1 | 2 | 4 | 1 | 1 | 4 | 1 |
| 2 | 3 | 6 | 2 | 2 | 8 | 2 |
| 4 | 4 | 8 | 4 | 2 | 16 | 2 |
| 8 | 4 | 8 | 8 | 2 | 32 | 2 |
Worker threads cap at 4 regardless of CPU count: SQLite has a single writer, so more threads add contention rather than throughput past that point. The other limits keep scaling with the CPU quota because they bound different, less contended resources.
Parallel fragments are the one limit re-evaluated per download rather than at startup: with Parallel fragments per download on Automatic, a host that is currently short on free memory gets fewer fragments than the CPU quota alone would allow (2 on 128 MB free, 4 on 256 MB, 8 from 512 MB up). A fixed value of 1–16 skips the probe.
Runtime settings and operator overrides¶
Settings → General → Runtime limits controls the download worker count, the parallel fragments per download (Automatic by default), the download and transcode timeouts, and the maximum source download size. A worker count of 0 means automatic sizing and applies after the next restart. The BPM-analysis limits and the Lalal result limit live in the Lalal.ai tile under Settings → Integrations.
The remaining resource-governor limits are operator-level overrides. 0 means "auto".
| Variable | Effect |
|---|---|
WORKER_QUEUE_MAXSIZE | Queue depth before submissions are rejected |
ANALYSIS_SEMAPHORE_LIMIT | Concurrent BPM analyses |
TRANSCODE_SEMAPHORE_LIMIT | Concurrent ffmpeg transcodes |
CPU_SEMAPHORE_LIMIT and IO_SEMAPHORE_LIMIT size two further semaphores that the governor creates and reports, but no code path currently acquires either one. They are reserved for a future gate — setting them has no effect on how much work runs in parallel. Download parallelism is shaped by Download workers and Parallel fragments per download instead.
Backpressure¶
| Variable | Default | Description |
|---|---|---|
ENABLE_BACKPRESSURE | on | Reject new jobs when resources are short |
MEMORY_THRESHOLD_MB | 256 | Stop accepting jobs below this much available memory |
With backpressure on, the queue is bounded and a submission that arrives with a full queue is rejected with 503 Service Unavailable rather than being accepted into a backlog that will never drain. Turning it off makes the queue unbounded — which trades a clear error for an eventual out-of-memory kill.
Timeouts¶
Download and transcode timeouts are configured in Settings → General → Runtime limits; the BPM-analysis timeout lives in Settings → Integrations → Lalal.ai. Container lifecycle timeouts remain deployment settings:
| Variable | Default | Applies to |
|---|---|---|
TIMEOUT | 60 s | Gunicorn worker (container) |
GRACEFUL_TIMEOUT | 15 s | Shutdown before SIGKILL (container) |
A subprocess that exceeds its timeout is terminated and the job is marked error with a message you can act on.
Sizing guidance¶
Set Download workers to 2 in Runtime limits. BPM analysis is the heaviest step; if memory is tight, keep the analysis semaphore at 1 or leave analysis to a bigger host.
Leave everything at auto. The defaults land on 4 workers, an 8-deep queue, and 2 concurrent transcodes.
Observing¶
Settings → System → Host resources shows storage, CPU, RAM, and uptime, sampled from /proc. Metrics that are unavailable degrade to empty rather than failing the panel.
Reading a full queue
Repeated 503s on submit mean the queue is saturated, not that something is broken. Either raise Download workers in Runtime limits (if the host has headroom) or accept that the host is at capacity — raising WORKER_QUEUE_MAXSIZE alone only hides the wait.