Skip to content

Architecture

Detailed overview of Torrentarr's system architecture and design patterns.

System Design

Torrentarr uses ASP.NET Core Generic Host with hosted background services, designed for reliability, scalability, and isolation:

graph TB
    Host["πŸŽ›οΈ Generic Host<br/>(Torrentarr.Host)"]

    Host -->|registers| WebAPI["🌐 ASP.NET Core API<br/>(minimal API endpoints)"]
    Host -->|registers| Radarr["πŸ“½οΈ Arr Manager Service<br/>(Radarr-4K)"]
    Host -->|registers| Sonarr["πŸ“Ί Arr Manager Service<br/>(Sonarr-TV)"]
    Host -->|registers| Lidarr["🎡 Arr Manager Service<br/>(Lidarr-Music)"]

    WebAPI -->|API calls| QBT["βš™οΈ qBittorrent<br/>(Torrent Client)"]
    Radarr -->|API calls| QBT
    Sonarr -->|API calls| QBT
    Lidarr -->|API calls| QBT

    Radarr -->|API calls| RadarrAPI["πŸ“‘ Radarr API"]
    Sonarr -->|API calls| SonarrAPI["πŸ“‘ Sonarr API"]
    Lidarr -->|API calls| LidarrAPI["πŸ“‘ Lidarr API"]

    WebAPI -.->|reads| DB[(πŸ—„οΈ SQLite<br/>Database)]
    Radarr -.->|writes| DB
    Sonarr -.->|writes| DB
    Lidarr -.->|writes| DB

    subgraph "Generic Host Responsibilities"
        H1["βœ… Lifetime management (start/stop)"]
        H2["βœ… Dependency injection container"]
        H3["βœ… Configuration pipeline"]
        H4["βœ… Graceful shutdown on SIGTERM/SIGINT"]
    end

    subgraph "ASP.NET Core API Responsibilities"
        W1["βœ… REST API (/api/*, /web/*)"]
        W2["βœ… React SPA (static files)"]
        W3["βœ… Token authentication"]
        W4["βœ… Real-time log streaming"]
    end

    subgraph "Arr Manager Responsibilities"
        A1["βœ… Independent background loop"]
        A2["βœ… Health monitoring"]
        A3["βœ… Import triggering"]
        A4["βœ… Blacklist management"]
    end

    style Host fill:#4dabf7,stroke:#1971c2,color:#000
    style WebAPI fill:#51cf66,stroke:#2f9e44,color:#000
    style Radarr fill:#ffa94d,stroke:#fd7e14,color:#000
    style Sonarr fill:#ffa94d,stroke:#fd7e14,color:#000
    style Lidarr fill:#ffa94d,stroke:#fd7e14,color:#000
    style QBT fill:#e599f7,stroke:#ae3ec9,color:#000
    style DB fill:#74c0fc,stroke:#1c7ed6,color:#000

Key Architecture Principles:

  • Service Isolation: Each Arr instance runs as an independent BackgroundService β€” one failure doesn't affect others
  • Fault Tolerance: The host monitors and restarts failed services via CancellationToken and retry policies
  • Simplicity: No complex IPC β€” coordination via SQLite and external APIs
  • Dependency Injection: All components are registered in the DI container for testability

Core Components

Generic Host

Project: Torrentarr.Host

The entry point β€” Program.cs β€” configures and starts the ASP.NET Core Generic Host:

  • Reads and validates configuration (TOML)
  • Registers all services in the DI container
  • Starts the ASP.NET Core HTTP server
  • Starts all IHostedService instances
  • Handles SIGTERM/SIGINT for graceful shutdown

ASP.NET Core API

Project: Torrentarr.Host β€” minimal API endpoints in Program.cs

Responsibilities:

  • Serves REST API on /api/* (token-protected) and /web/* (public) routes
  • Hosts React SPA from Torrentarr.Host/static/ via static file middleware
  • Provides token-based authentication middleware
  • Serves log files and process status to the WebUI

Arr Manager Services

Project: Torrentarr.Core β€” ArrManagerBase and subclasses

Each configured Arr instance (Radarr/Sonarr/Lidarr) runs as an IHostedService:

  • Independent background loop checking qBittorrent every N seconds
  • Queries Arr API for media information
  • Performs health checks on torrents
  • Triggers imports when torrents complete
  • Manages blacklisting and re-searching
  • Tracks state in SQLite database

Background Services

Auto-Update Service

  • Checks GitHub releases for new versions on a schedule
  • Downloads and validates release packages
  • Triggers application restart when an update is available

Configuration Watcher

  • Monitors config.toml for file-system changes
  • Signals running services to reload configuration
  • Triggers a RestartLoopException-equivalent via CancellationToken

Data Flow

Torrent Processing Pipeline

sequenceDiagram
    participant QBT as βš™οΈ qBittorrent
    participant AM as πŸ“‘ Arr Manager
    participant DB as πŸ—„οΈ Database
    participant ARR as 🎬 Arr API

    Note over AM: Every N seconds (LoopSleepTimer)

    rect rgb(230, 245, 255)
        Note right of AM: 1. Detection Phase
        AM->>QBT: GET /api/v2/torrents/info?category=radarr-4k
        QBT-->>AM: List of torrents with tags
        AM->>AM: Filter by configured categories
    end

    rect rgb(211, 249, 216)
        Note right of AM: 2. Classification Phase
        AM->>DB: SELECT * FROM Downloads WHERE Hash IN (...)
        DB-->>AM: Tracked torrents
        AM->>AM: Determine state:<br/>downloading, stalled,<br/>completed, seeding
    end

    rect rgb(255, 243, 191)
        Note right of AM: 3. Health Check Phase
        AM->>QBT: GET torrent details (ETA, stall time, trackers)
        QBT-->>AM: Torrent health data
        AM->>AM: Check ETA vs MaxETA<br/>Check stall time vs StallTimeout<br/>Verify tracker status
    end

    rect rgb(255, 230, 230)
        Note right of AM: 4. Action Decision Phase
        alt Completed + Valid
            AM->>ARR: POST /api/v3/command (DownloadedMoviesScan)
            ARR-->>AM: Import queued
            Note over AM: βœ… Import triggered
        else Failed Health Check
            AM->>ARR: POST /api/v3/queue/blacklist (hash)
            ARR-->>AM: Blacklisted
            AM->>QBT: DELETE /api/v2/torrents/delete
            Note over AM: ❌ Blacklisted & deleted
        else Blacklisted Item
            AM->>ARR: POST /api/v3/command (MoviesSearch)
            ARR-->>AM: Search queued
            Note over AM: πŸ” Re-search triggered
        else Seeded Enough
            AM->>QBT: DELETE /api/v2/torrents/delete
            Note over AM: πŸ—‘οΈ Cleaned up
        end
    end

    rect rgb(243, 232, 255)
        Note right of AM: 5. State Update Phase
        AM->>DB: UPDATE Downloads SET State=?, UpdatedAt=?
        AM->>DB: INSERT INTO EntryExpiry (EntryId, ExpiresAt)
        DB-->>AM: State persisted
        Note over AM: πŸ’Ύ Audit trail updated
    end

Pipeline Stages:

  1. Detection β€” Poll qBittorrent for torrents matching configured categories/tags
  2. Classification β€” Query database to determine tracking state and history
  3. Health Check β€” Evaluate torrent health against configured thresholds
  4. Action Decision β€” Choose appropriate action (import/blacklist/re-search/cleanup)
  5. State Update β€” Persist state changes and actions to database for audit trail

Configuration Flow

flowchart TD
    Start([πŸš€ Application Start])

    Start --> LoadTOML["πŸ“„ Load TOML File<br/>(config.toml)"]

    LoadTOML --> ParseTOML["πŸ” Parse & Validate<br/>(ConfigurationLoader.cs)"]

    ParseTOML --> CheckVersion{Config version<br/>matches?}

    CheckVersion -->|No| Migrate["βš™οΈ Apply Migrations<br/>(MigrateConfig)"]
    CheckVersion -->|Yes| EnvVars

    Migrate --> EnvVars["🌍 Config path override<br/>(TORRENTARR_CONFIG)"]

    EnvVars --> CheckEnv{TORRENTARR_CONFIG<br/>set?}

    CheckEnv -->|Yes| Override["✏️ Use as config file path<br/>(e.g. Docker)"]
    CheckEnv -->|No| Validate

    Override --> Validate["βœ… Validation<br/>(ValidateConfig)"]

    Validate --> CheckRequired{Required<br/>fields present?}

    CheckRequired -->|No| Error["❌ Error: Missing Config"]
    CheckRequired -->|Yes| DI["πŸ“¦ Register in DI Container<br/>(IOptions&lt;TorrentarrConfig&gt;)"]

    DI --> StartHost["πŸŽ›οΈ Start Generic Host"]

    StartHost --> StartWebAPI["Start β†’ 🌐 ASP.NET Core API"]
    StartHost --> SpawnArr1["Start β†’ πŸ“‘ Arr Manager 1"]
    StartHost --> SpawnArr2["Start β†’ πŸ“‘ Arr Manager 2"]

    StartWebAPI --> Runtime["⚑ Runtime"]
    SpawnArr1 --> Runtime
    SpawnArr2 --> Runtime

    Error --> End([πŸ’₯ Exit])
    Runtime --> End2([βœ… Running])

    style Start fill:#dee2e6,stroke:#495057,color:#000
    style LoadTOML fill:#e7f5ff,stroke:#1971c2,color:#000
    style Migrate fill:#fff3bf,stroke:#fab005,color:#000
    style Override fill:#d3f9d8,stroke:#2f9e44,color:#000
    style Validate fill:#e7f5ff,stroke:#1971c2,color:#000
    style Error fill:#ffe3e3,stroke:#c92a2a,color:#000
    style DI fill:#f3f0ff,stroke:#7950f2,color:#000
    style Runtime fill:#d3f9d8,stroke:#2f9e44,color:#000

Configuration Precedence (highest to lowest):

  1. Environment Variable (TORRENTARR_CONFIG) β€” Config file path only; when set, used to locate config.toml
  2. TOML File (config.toml) β€” Standard configuration
  3. Defaults (in ConfigurationLoader.cs) β€” Fallback values

Key Files:

  • Torrentarr.Core/Configuration/ConfigurationLoader.cs β€” TOML parsing, validation, migrations
  • Torrentarr.Host/Program.cs β€” DI registration, host startup

API Request Flow

sequenceDiagram
    participant Client as πŸ’» Client<br/>(React App/API)
    participant Auth as πŸ” Auth Middleware
    participant API as 🌐 ASP.NET Core API
    participant Logic as βš™οΈ Handler Logic
    participant DB as πŸ—„οΈ Database
    participant ARR as πŸ“‘ Arr APIs

    Client->>API: HTTP Request<br/>GET /api/processes

    rect rgb(255, 243, 191)
        Note right of API: Authentication Phase
        API->>Auth: Check Authorization header

        alt Token Valid
            Auth-->>API: βœ… Authenticated
        else Token Missing/Invalid
            Auth-->>Client: ❌ 401 Unauthorized
            Note over Client: Request rejected
        end
    end

    rect rgb(230, 245, 255)
        Note right of API: Request Processing Phase
        API->>Logic: Route to handler

        alt Read Operation
            Logic->>DB: SELECT * FROM Downloads
            DB-->>Logic: Query results
        else Write Operation
            Logic->>DB: INSERT/UPDATE/DELETE
            DB-->>Logic: Rows affected
        else External Query
            Logic->>ARR: GET /api/v3/movie/123
            ARR-->>Logic: Movie metadata
        end
    end

    rect rgb(211, 249, 216)
        Note right of API: Response Phase
        Logic-->>API: Processed data
        API->>API: Serialize to JSON
        API-->>Client: 200 OK<br/>{ "data": [...] }
    end

API Endpoints:

  • /api/processes β€” List all Arr manager services and their states
  • /api/logs β€” Read log files
  • /api/config β€” Read/update configuration
  • /web/status β€” Public status endpoint
  • /web/qbit/categories β€” qBittorrent category information

Authentication:

All /api/* endpoints require Authorization: Bearer header matching WebUI.Token from config.toml.

Component Architecture

Hosted Services Model

Torrentarr uses .NET's IHostedService / BackgroundService pattern:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Generic Host (Program.cs)                   β”‚
β”‚  - Configuration management                              β”‚
β”‚  - Dependency injection container                        β”‚
β”‚  - Service lifecycle orchestration                       β”‚
β”‚  - Signal handling (SIGTERM, SIGINT)                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                   β”‚ hosts
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚         β”‚         β”‚                 β”‚
    β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β” β”Œβ”€β”€β–Όβ”€β”€β”€β” β”Œβ”€β”€β”€β–Όβ”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”
    β”‚ASP.NET β”‚ β”‚Radarrβ”‚ β”‚ Sonarr β”‚ ... β”‚   Lidarr   β”‚
    β”‚  Core  β”‚ β”‚  Mgr β”‚ β”‚   Mgr  β”‚     β”‚    Mgr     β”‚
    β”‚        β”‚ β”‚      β”‚ β”‚        β”‚     β”‚            β”‚
    β”‚Minimal β”‚ β”‚Event β”‚ β”‚ Event  β”‚     β”‚   Event    β”‚
    β”‚  API   β”‚ β”‚Loop  β”‚ β”‚  Loop  β”‚     β”‚   Loop     β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚         β”‚         β”‚                 β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚  Shared Resources β”‚
                  β”‚  - SQLite DB      β”‚
                  β”‚  - Config file    β”‚
                  β”‚  - Log files      β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Service Registration (Program.cs):

var builder = WebApplication.CreateBuilder(args);

// Load Torrentarr configuration
builder.Services.Configure<TorrentarrConfig>(
    builder.Configuration.GetSection("Torrentarr"));

// Register Arr manager services
foreach (var arrConfig in config.ArrInstances.Values)
{
    builder.Services.AddHostedService(sp =>
        ArrManagerFactory.Create(arrConfig, sp));
}

// Register auto-update service
builder.Services.AddHostedService<AutoUpdateService>();

var app = builder.Build();

// Map API endpoints
app.MapGet("/web/status", GetStatus);
app.MapGet("/api/processes", GetProcesses).RequireAuthorization();
// ...

await app.RunAsync();

Database Architecture

Torrentarr uses SQLite for state persistence:

Schema

erDiagram
    Downloads ||--o| EntryExpiry : "has expiry"

    Downloads {
        string Hash PK "Torrent hash (SHA1)"
        string Name "Torrent name"
        string ArrType "radarr | sonarr | lidarr"
        string ArrName "Arr instance name"
        int MediaId "Movie/Series/Album ID in Arr"
        string State "downloading | stalled | completed | seeding"
        datetime AddedAt "When torrent was added to qBittorrent"
        datetime UpdatedAt "Last state update"
    }

    Searches {
        int Id PK "Auto-increment primary key"
        string ArrType "radarr | sonarr | lidarr"
        string ArrName "Arr instance name"
        int MediaId "Movie/Series/Album ID in Arr"
        string Query "Search query sent to Arr"
        datetime SearchedAt "When search was executed"
        int ResultCount "Number of results returned"
    }

    EntryExpiry {
        string EntryId FK "Foreign key to Downloads.Hash"
        datetime ExpiresAt "When to delete entry"
    }

Table Descriptions:

  • Downloads β€” Tracks all torrents Torrentarr is managing. Primary key is the torrent hash. Lifecycle: created on detection β†’ updated during health checks β†’ deleted after expiry.
  • Searches β€” Records all automated searches for audit and deduplication. Auto-cleaned after 30 days.
  • EntryExpiry β€” Schedules delayed cleanup after seeding goals are met.

Event Loop Architecture

Each Arr manager's background service loop:

flowchart TD
    Start([⚑ BackgroundService.ExecuteAsync])

    Start --> Init["πŸ”§ Initialize<br/>(load config, connect APIs)"]

    Init --> LoopStart{Cancellation<br/>requested?}

    LoopStart -->|Yes| Shutdown([πŸ›‘ Graceful Shutdown])
    LoopStart -->|No| FetchTorrents["πŸ“₯ Fetch Torrents<br/>qbitClient.GetTorrentsAsync(category)"]

    FetchTorrents --> QueryDB["πŸ—„οΈ Query Database<br/>SELECT * FROM Downloads"]

    QueryDB --> ProcessLoop["πŸ”„ Process Each Torrent"]

    ProcessLoop --> CheckTorrent{Torrent<br/>healthy?}

    CheckTorrent -->|Yes| Import["βœ… Trigger Import<br/>POST /api/v3/command"]
    CheckTorrent -->|No| Blacklist["❌ Blacklist & Delete<br/>POST /api/v3/queue/blacklist"]
    CheckTorrent -->|Stalled| Retry["⚠️ Retry or Re-search"]

    Import --> UpdateDB
    Blacklist --> UpdateDB
    Retry --> UpdateDB

    UpdateDB["πŸ’Ύ Update State<br/>UPDATE Downloads SET State=?"]

    UpdateDB --> Cleanup["πŸ—‘οΈ Cleanup Expired<br/>DELETE FROM Downloads WHERE ExpiresAt < NOW()"]

    Cleanup --> Sleep["πŸ’€ Sleep<br/>await Task.Delay(LoopSleepTimer, ct)"]

    Sleep --> LoopStart

    style Start fill:#dee2e6,stroke:#495057,color:#000
    style Shutdown fill:#ffe3e3,stroke:#c92a2a,color:#000
    style FetchTorrents fill:#e7f5ff,stroke:#1971c2,color:#000
    style Import fill:#d3f9d8,stroke:#2f9e44,color:#000
    style Blacklist fill:#ffe3e3,stroke:#c92a2a,color:#000
    style Retry fill:#fff3bf,stroke:#fab005,color:#000
    style Sleep fill:#f3f0ff,stroke:#7950f2,color:#000

BackgroundService implementation:

public abstract class ArrManagerBase : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await InitializeAsync(stoppingToken);

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                var torrents = await FetchTorrentsAsync(stoppingToken);
                var tracked = await GetTrackedTorrentsAsync(stoppingToken);

                foreach (var torrent in torrents)
                {
                    try
                    {
                        var health = await CheckHealthAsync(torrent, stoppingToken);

                        await (health switch
                        {
                            TorrentHealth.Completed => ImportAsync(torrent, stoppingToken),
                            TorrentHealth.Failed    => BlacklistAsync(torrent, stoppingToken),
                            TorrentHealth.Stalled   => HandleStalledAsync(torrent, stoppingToken),
                            _                       => Task.CompletedTask
                        });
                    }
                    catch (SkipTorrentException)
                    {
                        continue;
                    }
                }

                await UpdateStatesAsync(torrents, stoppingToken);
                await CleanupExpiredAsync(stoppingToken);

                await Task.Delay(_config.LoopSleepTimer, stoppingToken);
            }
            catch (OperationCanceledException)
            {
                break;  // Graceful shutdown
            }
            catch (ApiUnavailableException ex)
            {
                _logger.LogWarning("API unavailable: {Reason}. Retrying in {Delay}s",
                    ex.Reason, ex.RetryAfter.TotalSeconds);
                await Task.Delay(ex.RetryAfter, stoppingToken);
            }
        }
    }
}

Torrent State Machine

        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚ Detectedβ”‚ (New torrent found in qBittorrent)
        β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
             β”‚
        β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚ Downloading  β”‚
        β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚                 β”‚
β”Œβ”€β”€β”€β–Όβ”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”
β”‚Stalled β”‚      β”‚Completed β”‚
β””β”€β”€β”€β”¬β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
    β”‚                β”‚
β”Œβ”€β”€β”€β–Όβ”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”
β”‚Failed  β”‚      β”‚Importing β”‚
β””β”€β”€β”€β”¬β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
    β”‚                β”‚
β”Œβ”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”
β”‚Blacklisted β”‚  β”‚Imported  β”‚
β””β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
    β”‚                β”‚
β”Œβ”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”
β”‚Re-searchingβ”‚  β”‚ Seeding  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
                     β”‚
                β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”
                β”‚ Deleted  β”‚ (After seed goals met)
                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Security Architecture

Authentication

WebUI Token:

[WebUI]
Token = "your-secure-token"
  • All /api/* endpoints check Authorization: Bearer header
  • Token stored in config.toml (not in database)
  • React app reads token from localStorage
  • Stateless β€” no session management needed

Middleware registration:

app.Use(async (context, next) =>
{
    if (context.Request.Path.StartsWithSegments("/api"))
    {
        var token = context.Request.Headers.Authorization
            .ToString().Replace("Bearer ", "");

        if (token != cfg.WebUI.Token)
        {
            context.Response.StatusCode = 401;
            return;
        }
    }
    await next();
});

Network Binding

[WebUI]
Host = "127.0.0.1"  # Localhost only
Port = 6969
  • Default: 0.0.0.0 for Docker
  • Recommended: 127.0.0.1 behind a reverse proxy for native installs
  • No TLS built-in β€” use nginx/Caddy for HTTPS

Performance Characteristics

Resource Usage

Typical Load (4 Arr instances, 50 torrents):

  • CPU: 1-2% average, 5-10% during health checks
  • RAM: 150-300 MB (.NET runtime + application)
  • Disk I/O: Minimal (SQLite writes are infrequent)
  • Network: 1-5 KB/s (API polling)

Scaling:

  • Each Arr instance adds ~20-30 MB RAM (background service overhead)
  • Check interval trades CPU for responsiveness
  • Database size grows with torrent history

Bottlenecks

  1. SQLite Write Contention β€” Mitigated by short-lived transactions; future: PostgreSQL support
  2. Arr API Rate Limits β€” Batched requests, retry with backoff
  3. qBittorrent API Overhead β€” Fetch only needed fields, cache responses

Extensibility

Adding New Arr Types

  1. Subclass ArrManagerBase in Torrentarr.Core
  2. Implement CheckHealthAsync() and HandleFailedAsync()
  3. Register as a hosted service in Program.cs
  4. Add config section to TorrentarrConfig

Adding New API Endpoints

// In Program.cs β€” minimal API style
app.MapGet("/api/myfeature", async (IMyService svc) =>
{
    var result = await svc.GetDataAsync();
    return Results.Ok(result);
}).RequireAuthorization();

Further Reading