Architecture - Kheopsian/Hydranos GitHub Wiki

🇬🇧 English · 🇫🇷 Français

Architecture

Applies to Hydranos 4.4.

For readers who want to know how Hydranos is built: what runs inside the process, in which order it starts, and where every byte of state lives on disk. For the short mental model, read Concepts and Glossary first. Coming from 3.x? See Upgrading from 3.x.

Binaries

Binary What it is Shipped in
hydranos The daemon: web UI, native REST API, qBittorrent shim, MCP server and every BitTorrent engine, in one process. Docker image, Linux tarball, Windows zip
hydranos-update The self-updater. A separate binary because Windows cannot replace a running executable. It replaces binaries only, never your config or data. Linux tarball, Windows zip (not the Docker image)

Releases no longer ship the legacy hydranos-engine binary.

hydranos accepts --config <path> (default /config/default.toml; on Windows default.toml beside hydranos.exe), --console (Windows: open a console window), --version (prints hydranos and the version) and --help, plus two commands, hydranos reset-password <password> [config] and hydranos hash-password <password>. Any other argument is named in a warning on stderr (ignoring unknown argument …; starting anyway), and the 3.x --agent-* / --front-* flags get a warning pointing to nodes; neither stops the start. Full list with environment variables and signals: Configuration: Environment Variables, CLI and Signals.

What runs inside the process

Everything is one process: no child process, no second program to supervise. An HTTP handler reads an engine's torrent table directly in memory.

  • HTTP listener on api_host:api_port (default 0.0.0.0, port 8199). It opens first, behind a startup gate that serves the startup screen and its /static/* files, /health, /metrics, /api/startup and GET /api/setup, and answers 503 to every other path (another method on one of these, such as POST /api/setup, is not routed and is refused) until the catalogue is loaded (see Startup sequence). Then one router serves:
    • / and /static/*: the web UI. Its HTML, JavaScript and translations are compiled into the binary; there is no web folder to install.
    • /api/*: the native REST API (including /api/events, the live update stream the UI listens to).
    • /api/v2/*: the qBittorrent-compatible shim.
    • /mcp: the MCP server for AI clients.
    • /health and /metrics: liveness and the Prometheus text page (what they report: Monitoring and Logs, Prometheus Metrics).
    • /install.sh: the node enrolment script (see Nodes and Multiple Machines).
  • Engine host: holds the list of engines built at boot (race, hoard, then any extra engine), and the managed WireGuard tunnels it brought up for them. Engines are never added or removed while running: a change takes a restart.
  • Daemon egress client: the one HTTP client for requests that belong to no engine (tracker and IP-filter lists, update check, webhooks, .torrent URLs, the process's exit-IP probe), routed by [daemon] egress and replaced without a restart when the config changes (see VPN and Egress).
  • Engines: one BitTorrent session each (next section).
  • Background workers, all started by hydranos itself:
Worker Scope What it does
Announce scheduler one per engine Announces every torrent of that engine; behaviour follows the engine's role (see Trackers and Announces). It also scrapes the torrents no tracker has answered for yet: first pass 90 s after start, then every 300 s, at most 5000 torrents a pass.
Resume sweep one per engine Every 300 s, writes the changed rows of the engine's state.db.
Stagger start one per engine, at boot Starts the non-stopped torrents in batches of 2000 with 100 ms pauses.
Queue manager one per engine First pass 60 s after start, then every 30 s, on the live config: active_downloads, and with queueing = true active_seeds / active_limit; slow torrents (see Queue, Share and Speed Limits).
Share-limit worker one per engine First pass 90 s after start, then every 120 s: stops or removes seeds past their ratio or seeding-time limits, at most 500 actions a pass.
Choker one per engine Every 10 s; does nothing unless choking = true.
LSD one per engine where enable_lsd is on Local peer discovery (BEP 14), pinned like the engine's other sockets (see Networking).
Port followers one per engine in a managed WireGuard tunnel with NAT-PMP, or with gluetun_port_forward Keep the port granted by the tunnel's gateway (NAT-PMP) or held by gluetun, and move the port the engine announces when it changes (behind gluetun, its listener too).
Seed-time sync one per engine Copies seed time from the engine into the store once a day (first pass one hour after boot).
Race drain one per engine whose role is race Frees race volumes (see Race Drain and Graduation).
Transit sweep one per engine Every 300 s (first pass 90 s after start), deletes, with their files, the torrents whose seed obligation is met, in categories that are marked Transit area and also named as another category's Graduate to (hoard category).
Jobs runner process Runs persistent jobs from the Jobs tab, one at a time. A separate watcher checks, every 30 s, moves to a node that wait for the node to hold a full copy.
Workflows runner process Timer workflows, plus workflows triggered by completed and added torrents.
RSS process Every 30 s (at once when a feed or rule changes), fetches the feeds that are due, 4 at a time, and runs the download rules (see RSS and Automatic Downloads).
Magnet resolver process Fetches metadata for pending magnets, including those a restart interrupted.
Watched folders process Polls folders for .torrent / .magnet files.
IP filter process Keeps block lists and bans up to date.
Line test process Only when [vpn_speedtest] interval_secs is above 0: a test every that many seconds, 300 at least.
Port mapping one per engine on the network NAT-PMP, then UPnP, when auto_port_forward is true (the default). Not for an engine that is blocked, in a managed tunnel, behind gluetun or a SOCKS5 proxy, or pinned to another interface than the default route's.
Exit-IP probe process Every 180 s, measures the public address of the daemon's own requests (through [daemon] egress), and of each engine that has a bind_interface and is not blocked.
Store reconcile process Every 5 minutes, removes store rows of torrents their engine no longer holds (with safety limits).
Anomaly scan process First pass 2 minutes after start, then every 5 minutes; serves /api/health/anomalies and the MCP health tool (see Monitoring and Logs).
Link scanner, benchmark sampler, race event recorder process Feed hardlink columns, the Benchmark tab and race history.
Tray icon Windows only Open Hydranos, Check for updates, Quit Hydranos.
  • Tokio runtime: one multi-thread runtime for the whole process. Worker threads = HYDRANOS_WORKER_THREADS if set and greater than 0, else the number of CPU cores, capped at 32.

Process map

flowchart TB
    clients["Browser, Sonarr, Radarr, autobrr, scripts, MCP clients"]
    peers["Peers, trackers, DHT nodes"]
    net_daemon["Tracker and IP-filter lists, update check,<br/>webhooks, .torrent URLs"]

    subgraph proc["hydranos process - one Tokio runtime"]
        http["HTTP listener<br/>0.0.0.0:8199 by default<br/>startup gate until ready"]
        ui["/ and /static/*<br/>web UI, compiled in"]
        api["/api/*<br/>REST API and /api/events"]
        qbit["/api/v2/*<br/>qBittorrent shim"]
        mcp["/mcp<br/>MCP server"]
        mon["/health and /metrics"]
        core["Shared state<br/>live config, store handle, engine host"]
        workers["Background workers<br/>queue, share limits, drain, transit sweep,<br/>jobs, workflows, RSS, magnets, watched folders,<br/>IP filter, port mapping, exit-IP probe,<br/>reconcile, anomaly scan"]
        egress["Daemon egress client<br/>route from daemon.egress"]

        subgraph eng_race["engine race - role race"]
            race_tm["Torrent table and disk I/O"]
            race_net["Listener 16171 TCP and uTP,<br/>dial queue, DHT, LSD"]
            race_ann["Announce scheduler, Race mode,<br/>and scrape"]
        end

        subgraph eng_hoard["engine hoard - role hoard"]
            hoard_tm["Torrent table and disk I/O"]
            hoard_net["Listener 16172 TCP and uTP,<br/>dial queue, DHT, LSD"]
            hoard_ann["Announce scheduler, Hoard mode,<br/>and scrape"]
        end

        subgraph eng_extra["extra engine, e.g. vpn1"]
            extra_tm["Torrent table and disk I/O"]
            extra_net["Own listener, dial queue, DHT, LSD"]
            extra_ann["Announce scheduler, mode from its role,<br/>and scrape"]
        end
    end

    way_race["race's way out<br/>default route, bind_interface, tunnel or proxy"]
    way_hoard["hoard's way out"]
    way_extra["vpn1's way out, e.g. wg-vpn1"]
    store[("data_dir/hydra.db<br/>the store")]
    race_sdb[("race/state.db")]
    hoard_sdb[("hoard/state.db")]
    extra_sdb[("vpn1/state.db")]

    clients --> http
    http --> ui
    http --> api
    http --> qbit
    http --> mcp
    http --> mon
    api --> core
    qbit --> core
    mcp --> core
    mon --> core
    workers --> core
    workers -->|"lists, webhooks, update check"| egress
    egress --> net_daemon
    core -->|"catalogue writes"| store
    core --> race_tm
    core --> hoard_tm
    core --> extra_tm

    race_tm -->|"metainfo, read-only"| store
    hoard_tm -->|"metainfo, read-only"| store
    extra_tm -->|"metainfo, read-only"| store
    race_tm -->|"resume rows"| race_sdb
    hoard_tm -->|"resume rows"| hoard_sdb
    extra_tm -->|"resume rows"| extra_sdb

    race_tm --- race_net
    race_tm --- race_ann
    hoard_tm --- hoard_net
    hoard_tm --- hoard_ann
    extra_tm --- extra_net
    extra_tm --- extra_ann
    race_net --> way_race
    race_ann --> way_race
    hoard_net --> way_hoard
    hoard_ann --> way_hoard
    extra_net --> way_extra
    extra_ann --> way_extra
    way_race --> peers
    way_hoard --> peers
    way_extra --> peers
Loading

Each engine's dials, announces, scrapes, DHT and LSD leave by that engine's own way out; peers found by its announces, DHT, PEX or LSD go into its own dial queue. An engine the kill switch blocks has none of the network boxes above: it holds its catalogue and answers the API.

Engines

An engine is one BitTorrent session inside the process. Every instance has race and hoard; extra engines come from engine blocks in the config file (see Engines: Race and Hoard). Each engine owns:

  • a listen port: TCP peer listener plus a uTP socket on the same UDP port (16171 for race and 16172 for hoard in the shipped default.toml; a missing key means 16171 for the race role and 16172 for the hoard role, and a port another engine already holds is replaced by a free one at start, with a warning: set it explicitly for every extra engine);
  • its own outbound dial queue, with its own binding, peer id, uTP socket and connection limits;
  • an optional interface pin (bind_interface, Linux only) that carries every socket of the engine: listener, uTP, dials, HTTP and UDP announces, scrapes, DHT, LSD, webseeds. Outside Linux an engine with bind_interface stays off the network;
  • a DHT node, unless enable_dht = false for that engine (absent means on), bound to the engine's interface when it has one (off behind a SOCKS5 proxy);
  • LSD (local peer discovery), if enable_lsd is on for it (absent: on for the race role, off otherwise);
  • an announce scheduler, in Race mode if the engine's role is race, else Hoard mode, with its own tracker clients on the engine's way out;
  • its own peer id (-HY, the version, -, then 12 random characters, drawn again at every start), in its announces and in every handshake;
  • its own resume DB, <config dir>/<engine id>/state.db;
  • its own read cache; the file-handle cache is shared by all engines.

Since 4.4 nothing an engine sends goes through another engine's binding: each one is a separate network identity. What leaves outside the engines (the daemon's own requests, tracker names resolved by the host for an interface Hydranos did not create) is listed on VPN and Egress.

If an engine's listener cannot bind (for example its bind_interface is not up), the engine still holds its catalogue and answers the API; /api/engines reports it as not listening. An engine the kill switch blocks (armed, as it is by default in every mode but Direct, with no tunnel, interface, proxy or allow_direct of its own) is never put on the network at all: no listener, dial, announce, DHT, LSD or port mapping.

Startup sequence

This is the order in the code. Two points surprise people: the engines go on the network before the shared store handle is opened and its schema checked, and the HTTP port opens first but serves only a startup screen until the very end. Until then the UI shows a progress bar and every API answers 503 with Retry-After: 5 (details: Operating Hydranos).

  1. Build the Tokio runtime; on Linux/macOS, start the allocator diagnostics.
  2. Read the command line (reset-password and hash-password run and exit here; unknown arguments are warned about). If the config file does not exist, write one from the built-in template, with data_dir set to <config dir>/data.
  3. Start logging: stderr, the in-memory ring the Logs tab reads (2000 lines), and <config dir>/hydranos.log.
  4. Read HYDRANOS_STOP_TIMEOUT. Load the config. If [daemon] api_key is empty or still the published placeholder, generate a key and write it into the file. Name the keys the file holds that nothing reads (config: … is ignored). Set the route of the daemon's own requests ([daemon] egress, kill switch).
  5. Create data_dir if missing.
  6. Bind the HTTP listener on api_host:api_port, behind the startup gate. The log says hydra API listening (starting). A port already taken fails here, in the first second. SIGTERM and Ctrl+C are heard from now on.
  7. Load every engine, in the order race, hoard, then extras: for each, open hydra.db read-only for metainfo and read-write to import any uploads/<info_hash>.torrent whose blob is missing (runs every start, finds nothing once migrated), and load the resume rows from its state.db. No socket is open yet.
  8. In WireGuard, managed by Hydranos mode, bring up the tunnels wg-<engine> first. Then decide which engines the kill switch keeps off the network: each one is logged kill switch: BLOCKED, kept off the network … and skipped. Then, in the same order, put each other engine on the network: for a NAT-PMP tunnel, ask its gateway for the port; DHT, LSD, uTP, TCP listener, dial queue, choker; announce scheduler (with its scrape), gluetun or tunnel port follower; stagger start, verify throttle. An engine with start_paused = true holds its dials and announces until released. A stop requested during the load ends here: resume flush, tunnels down, exit.
  9. Open the shared hydra.db handle read-write and check its schema. If that fails, Hydranos enters rescue mode (below) and goes no further.
  10. Log what the kill switch covers (kill switch: …, one line per engine and one for the daemon) and the ignored 3.x category fields. Start port mapping for each engine on the network that may be mapped on the home router (if auto_port_forward).
  11. Open bench.db (a missing or broken one is tolerated) and start the race event recorder.
  12. In WAL mode, open a second read-only store connection and start the 1 s checkpointer; start the store reconcile and the content-index backfill.
  13. Re-apply the stop state you set on torrents (from the store), without sending trackers a stopped event; start each engine's seed-time sync.
  14. One-time migration for configs that still contain [announce_clients] (see Upgrading from 3.x).
  15. Start the benchmark sampler, the exit-IP probe and the day counters; restore the client-wide speed caps from the store.
  16. Start the race drain on each race-role engine, then on every engine the transit sweep, the queue manager and the share-limit worker.
  17. Start the jobs runner and the node-handoff watcher, workflows, magnet resolver, line test, IP filter, watched folders, RSS, link scanner and anomaly scan.
  18. Windows: listen for the updater's stop request and show the tray icon.
  19. Hand the real router to the listener. The log says hydra API ready. The UI and every API answer from this point.
sequenceDiagram
    participant M as hydranos main
    participant C as default.toml
    participant H as HTTP listener
    participant E as Engine host
    participant S as hydra.db store
    participant N as Network

    M->>C: seed from built-in template if missing
    M->>M: logging to stderr, Logs ring, hydranos.log
    M->>C: load, write a generated api_key if empty or placeholder
    M->>M: name ignored keys, set the daemon egress route
    M->>M: create data_dir
    M->>H: bind api_host and api_port - startup screen, 503 elsewhere
    loop each engine - race, hoard, extras
        E->>S: open read-only, for metainfo
        E->>S: open read-write, import missing uploads/ blobs
        E->>E: load resume rows from its state.db
    end
    E->>N: managed WireGuard tunnels up, if that mode
    loop each engine not blocked by the kill switch
        E->>N: DHT, LSD, uTP and TCP listener, dial queue
        E->>N: announce scheduler and scrape, port follower, stagger start
    end
    M->>S: open read-write, check schema
    alt store cannot open or schema check fails
        M->>H: rescue routes on the same listener
    else store open
        M->>N: NAT-PMP or UPnP per engine that may be mapped
        M->>M: bench.db, checkpointer, reconcile, backfill
        M->>E: re-apply stopped torrents from the store
        M->>M: seed-time sync, drain, transit sweep, queue, share limits
        M->>M: jobs, workflows, RSS, magnets, IP filter, watched folders, anomaly scan
        M->>H: hand over the router - UI and APIs reachable
    end
Loading

Rescue mode

If hydra.db cannot be opened, or its torrents table lacks expected columns, Hydranos serves a minimal rescue surface on the listener it already opened (your api_host and api_port) and nothing else. It serves no web UI: / answers a one-line text message, and the only other routes are a health check, the store diagnosis and repair routes, and a restart. The engines were already put on the network in step 8 and keep running. The routes, the repair and how to use it are on Operating Hydranos.

Persistent state

Hydranos keeps two kinds of SQLite databases you must back up (the store and each engine's resume state), plus a telemetry SQLite file (bench.db), a log file and, in managed WireGuard mode, the provider files in <data_dir>/wireguard (Managed WireGuard). The split is deliberate: the store owns what you decided (which torrents exist, their metainfo, category, tags, stop intent, pins), each engine's resume state owns what the engine measured (progress, bytes moved, seed time).

The store (hydra.db)

<data_dir>/hydra.db is the catalogue. It holds:

Table Content
torrents One row per torrent per engine, key (info_hash, session) where session is the engine id: the .torrent bytes as a BLOB, save path, category, added and completed time, lifetime bytes, stop intent (paused), tags, pinned (force download), seed time (daily fallback copy).
counters Lifetime carry-over bytes of removed torrents, global and per tracker.
meta JSON documents: categories, provenance, per-volume drain policy (marks, quota, TRIM after drain).
tag_registry Known tags.
drain_history Race drain passes.
jobs The job queue.
workflows, workflow_activity, workflow_events Workflow definitions, their activity log, pending events.
magnets Magnets waiting for metadata, with their add options (they survive a restart).
nodes, enrol_tokens Declared nodes and one-time enrolment tokens.
app_settings, ip_bans UI-written settings (IP filter, watched folders, the client-wide speed caps client_rate_limits) and bans.
content_index, link_index Duplicate-data detection and hardlink scan results.
share_limits Share limits set on single torrents (sparse: only torrents that have their own).
rss_feeds, rss_folders, rss_articles, rss_rules, rss_taken RSS feeds, their articles, the download rules and the articles already taken.

The store is the only source of metainfo. At startup each engine reads its resume rows, then fetches each torrent's .torrent from hydra.db by info hash. There is no fallback to .torrent files on disk: losing hydra.db loses the catalogue, even if every state.db and every payload file is intact. Back it up (see Operating Hydranos; there is no built-in backup).

Journal mode:

  • On a local filesystem the store runs in WAL mode (synchronous=NORMAL), with a background thread checkpointing every second. You will see hydra.db-wal and hydra.db-shm beside it; they are part of the store.
  • If the folder holding hydra.db is detected as a network filesystem, the store keeps a classic rollback journal. On Linux "network" means NFS, SMB/CIFS or FUSE, so a store on a mergerfs or Unraid user share runs without WAL. On Windows it means a UNC path or a mapped network drive. Other systems are always treated as local.
  • synchronous=NORMAL means a power cut can lose the last few commits; a crash of the daemon alone cannot.

When the store is opened read-write, missing tables are created; an older single-key torrents table is rebuilt on (info_hash, session) once.

Resume state (/state.db)

Each engine has <config dir>/<engine id>/state.db, with one row per torrent: save path, seed mode, stop flag, uploaded and downloaded bytes, added and completed time, the bitfield of verified pieces, the tracker list (so tracker edits survive a restart), seed seconds, the torrent's own speed caps and its download order (file priorities, sequential). It is always opened in WAL mode, whatever the filesystem: the network-filesystem check that keeps the store out of WAL does not apply to it. Keep the config dir on a local disk (on Unraid, not a FUSE user share).

It is written immediately when a torrent is added, completes or changes save path, and otherwise by the 300 s sweep, which writes only rows that changed. Old per-torrent files in <config dir>/<engine id>/resume/ are imported once into an empty state.db and left in place. state.db sits under the config dir, not data_dir.

bench.db

<data_dir>/bench.db holds the telemetry behind the Benchmark tab and records. Hydranos starts without it if it cannot be opened, and nothing else depends on it.

hydranos.log

<config dir>/hydranos.log, rotated by Hydranos itself at 128 MiB, with 4 older files kept (hydranos.log.1 to .4). Setting HYDRANOS_LOG_STDOUT to any value (even 0) disables the file. Details: Monitoring and Logs.

uploads/ (one-time migration only)

<data_dir>/uploads/ is never written by 4.4. If it exists, each engine imports the .torrent files the store is missing at startup; after that the folder is not read for anything.

Where data lives

"Config dir" is the folder that contains the config file; it holds the engine folders (race/, hoard/, one per extra engine) and hydranos.log. data_dir comes from [daemon] data_dir and holds hydra.db and bench.db. Your torrent payloads live wherever their save paths point, never in either folder.

Install Config file Config dir (engine folders, log) data_dir (store, bench)
Docker (stock image) /config/default.toml /config /config
Linux, packaging/install.sh + systemd /etc/hydranos/default.toml /etc/hydranos /var/lib/hydranos
Binary started without --config (Linux) /config/default.toml /config /config/data
Windows zip, no --config default.toml beside hydranos.exe (an install 4.3 seeded at \config\default.toml keeps using it; see Install on Windows) the folder of hydranos.exe <exe folder>\data
  • Docker: the image's entrypoint seeds /config/default.toml with data_dir = "/config" (it follows HYDRANOS_CONFIG_DIR if you set it), so everything lives in the /config volume.
  • Binary: when hydranos writes a starting config itself, it sets data_dir to <config dir>/data. Pass --config to choose another place; the config dir and data_dir follow.
  • Packaged Linux: state.db files and the log land under /etc/hydranos/race/, /etc/hydranos/hoard/ and /etc/hydranos/hydranos.log, beside the config. See Install on Linux (bare metal).
flowchart LR
    subgraph cfgdir["config dir - folder of default.toml"]
        toml["default.toml"]
        log["hydranos.log"]
        rdb[("race/state.db")]
        hdb[("hoard/state.db")]
        xdb[("extra engine id/state.db")]
        rjson["engine id/resume/<br/>legacy, imported once"]
    end
    subgraph datadir["data_dir - from daemon.data_dir"]
        hydra[("hydra.db<br/>plus -wal and -shm in WAL mode")]
        bench[("bench.db")]
        up["uploads/<br/>legacy, imported once"]
        wgdir["wireguard/<br/>provider .conf files, managed.list"]
    end
    payload["Payload files<br/>at each torrent's save path"]

    toml -->|"data_dir points to"| hydra
    rdb -->|"metainfo looked up by info hash"| hydra
    hdb -->|"metainfo looked up by info hash"| hydra
    xdb -->|"metainfo looked up by info hash"| hydra
    up -.->|"missing blobs copied at startup"| hydra
    rjson -.->|"imported into an empty state.db"| rdb
    rdb -->|"save path"| payload
    hdb -->|"save path"| payload
Loading

Request paths

A list row on the Race or Hoard tab combines two sources: live state (progress, speeds, peers, tracker status) read from the engine's memory, and the facts you set (category, tags, stop intent, force download) read from the store. A write that changes what you decided goes to the store. A new torrent (REST add, shim, UI, MCP, watched folder, magnet) is written to the store before the engine is told, because the engine reads its piece hashes from the store. Copies to a second engine and job moves reuse metainfo already in the store, so they tell the engine first and write the row after.

flowchart LR
    req_list["GET list page<br/>REST API or UI"]
    req_add["Add a torrent<br/>REST API, shim, UI, MCP, watched folder"]
    check{"Save path known, race add-block<br/>and path claim pass?"}
    req_live["GET /api/events<br/>live updates"]
    engmem["Engine memory<br/>progress, speeds, peers"]
    store[("hydra.db")]
    sdb[("engine state.db")]
    refuse["Refused: no save path, race add-block<br/>or path claim set to refuse"]

    req_list --> engmem
    req_list --> store
    req_live --> engmem
    req_add --> check
    check -->|"no"| refuse
    check -->|"yes, step A: row with .torrent bytes"| store
    store -->|"step B: then the engine gets it"| engmem
    engmem -->|"step C: resume row written at once"| sdb
Loading

The path claim can also add the torrent in a folder of its own, or paused until files already on disk are proven, when another torrent names the same files. The full add pipeline (categories, path claim, duplicate data, on-add workflows) is on Adding Torrents and Existing Data.

One torrent, several engines

The same torrent can live in more than one engine of the same instance: store rows are keyed (info_hash, session), so each copy has its own row (category, tags, stop intent) and each engine its own resume row. The .torrent bytes are looked up by info hash only, so all copies share one metainfo.

  • POST /api/torrents/:info_hash/copy with {"engine": "<id>"} (Duplicate to engine ›) adds the torrent to a second engine, pointed at the same save path (same files, no second copy on disk), in seed mode. Category and added time are copied, tags are not.
  • POST /api/torrents/:info_hash/engine with {"engine": "<id>"} (Move to engine ›) moves one copy to another engine of the same instance with its resume record (pieces, counters, tracker edits, seeding time, stop state); the data does not move.
  • Per-torrent REST routes take ?engine=<id> to say which copy they act on. qBittorrent shim actions (stop, start…) apply to every copy.

In the web UI both are in the context menu's Node group, targets local-<id>: see Putting torrents on an extra engine on Engines: Race and Hoard.

Crash safety

Event What is saved What can be lost
Graceful stop: SIGTERM or SIGINT (Linux), Ctrl+C, Ctrl+Break, closing the console, sign-out, system shutdown or tray Quit Hydranos (Windows) HTTP server drains (5 s at most), stopped is sent to the trackers (5 s at most), then every engine's resume state is written in full, one thread per engine; managed tunnels go down last. Nothing, if the flush fits in HYDRANOS_STOP_TIMEOUT (default 120 s; 120, 60s, 2m or 1m30s) and the supervisor waits that long. Past the budget, Hydranos leaves with what is written. Windows gives only a few seconds after a sign-out or shutdown, so that flush may be partial.
Restart from the UI or API (POST /api/restart, POST /api/settings/restart) The same clean stop, then exit code 75 (Windows: starts its successor first). As for a graceful stop. A supervisor that restarts on failure brings it back.
kill -9, crash, Task Manager kill Everything already committed: store rows, and resume rows written on add, completion and save-path change. Up to 300 s of piece progress, byte counters and seed time; the next start re-checks what was lost.
Power cut As above, minus the last few commits of each WAL database. The last few seconds of store and resume writes, including a torrent added in those seconds.

Torrents added before a daemon crash or kill -9 are never lost (a power cut can still drop one added in its last seconds): their store row and resume row are written at add time. Jobs that were running are re-queued at the next start; magnets waiting for metadata resume. Supervisor budgets (stop_grace_period, TimeoutStopSec) and the restart exit code are covered on Operating Hydranos.

Multiple machines

Each machine runs its own complete hydranos with its own store and engines. Instances talk to each other over plain HTTP, through each other's normal API, authenticated with the other instance's API key. The one exception is enrolment: the new machine registers once with the controller using a one-time token. See Nodes and Multiple Machines.

⚠️ **GitHub.com Fallback** ⚠️