Architecture - Kheopsian/Hydranos GitHub Wiki
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.
| 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.
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(default0.0.0.0, port 8199). It opens first, behind a startup gate that serves the startup screen and its/static/*files,/health,/metrics,/api/startupandGET /api/setup, and answers 503 to every other path (another method on one of these, such asPOST /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. -
/healthand/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,
.torrentURLs, the process's exit-IP probe), routed by[daemon] egressand replaced without a restart when the config changes (see VPN and Egress). - Engines: one BitTorrent session each (next section).
-
Background workers, all started by
hydranositself:
| 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_THREADSif set and greater than 0, else the number of CPU cores, capped at 32.
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
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.
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 withbind_interfacestays off the network; - a DHT node, unless
enable_dht = falsefor 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_lsdis 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.
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).
- Build the Tokio runtime; on Linux/macOS, start the allocator diagnostics.
- Read the command line (
reset-passwordandhash-passwordrun and exit here; unknown arguments are warned about). If the config file does not exist, write one from the built-in template, withdata_dirset to<config dir>/data. - Start logging: stderr, the in-memory ring the Logs tab reads (2000 lines), and
<config dir>/hydranos.log. - Read
HYDRANOS_STOP_TIMEOUT. Load the config. If[daemon] api_keyis 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). - Create
data_dirif missing. - Bind the HTTP listener on
api_host:api_port, behind the startup gate. The log sayshydra API listening (starting). A port already taken fails here, in the first second. SIGTERM and Ctrl+C are heard from now on. - Load every engine, in the order
race,hoard, then extras: for each, openhydra.dbread-only for metainfo and read-write to import anyuploads/<info_hash>.torrentwhose blob is missing (runs every start, finds nothing once migrated), and load the resume rows from itsstate.db. No socket is open yet. - 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 loggedkill 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 withstart_paused = trueholds its dials and announces until released. A stop requested during the load ends here: resume flush, tunnels down, exit. - Open the shared
hydra.dbhandle read-write and check its schema. If that fails, Hydranos enters rescue mode (below) and goes no further. - 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 (ifauto_port_forward). - Open
bench.db(a missing or broken one is tolerated) and start the race event recorder. - 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.
- Re-apply the stop state you set on torrents (from the store), without sending trackers a
stoppedevent; start each engine's seed-time sync. - One-time migration for configs that still contain
[announce_clients](see Upgrading from 3.x). - Start the benchmark sampler, the exit-IP probe and the day counters; restore the client-wide speed caps from the store.
- Start the race drain on each race-role engine, then on every engine the transit sweep, the queue manager and the share-limit worker.
- Start the jobs runner and the node-handoff watcher, workflows, magnet resolver, line test, IP filter, watched folders, RSS, link scanner and anomaly scan.
- Windows: listen for the updater's stop request and show the tray icon.
- 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
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.
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).
<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 seehydra.db-walandhydra.db-shmbeside it; they are part of the store. - If the folder holding
hydra.dbis 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=NORMALmeans 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.
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.
<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.
<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.
<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.
"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.tomlwithdata_dir = "/config"(it followsHYDRANOS_CONFIG_DIRif you set it), so everything lives in the/configvolume. -
Binary: when
hydranoswrites a starting config itself, it setsdata_dirto<config dir>/data. Pass--configto choose another place; the config dir anddata_dirfollow. -
Packaged Linux:
state.dbfiles 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
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
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.
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/copywith{"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/enginewith{"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.
| 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.
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.