Skip to content

Deployment Topologies

Kheopsian edited this page Oct 12, 2026 · 12 revisions

🇬🇧 English · 🇫🇷 Français

Deployment Topologies

Applies to Hydranos 4.4.

This page is for you if you are deciding how to lay Hydranos out: one instance, one instance with more engines, or several instances on several machines. At the end you will know what each shape gives you, what it does not, how to reach the web UI from outside, and what the project has measured for a large catalogue.

Every shape is built from the same thing: one hydranos process with its config file, its store and at least two engines. There is no special "controller build" or "headless build"; a node is just another whole instance.

One instance

This is what every install gives you: one process, two engines (race and hoard), one HTTP listener on port 8199 for the web UI, the REST API, the qBittorrent shim and the MCP server. Each engine has its own listen port: 16171 (race) and 16172 (hoard), TCP for peers and UDP for uTP on the same number. The full ports table lives on Installation and First Run.

flowchart LR
    users["Browser, Sonarr/Radarr, autobrr, scripts"] -->|"HTTP, port 8199"| api
    subgraph inst["hydranos instance"]
        api["HTTP listener<br/>UI, /api, /api/v2, /mcp"]
        race["engine race<br/>port 16171 TCP+UDP"]
        hoard["engine hoard<br/>port 16172 TCP+UDP"]
        store[("store hydra.db")]
        api --> race
        api --> hoard
        api --> store
    end
    race <-->|"peers"| swarm["BitTorrent peers"]
    hoard <-->|"peers"| swarm
    race -->|"announces"| trackers["Trackers"]
    hoard -->|"announces"| trackers
Loading

A category's Mode decides which of the two engines a torrent lands in (rule on Categories and Routing); the role differences are on Engines: Race and Hoard.

One instance is enough when:

  • one machine holds the data you seed, and
  • one network exit is fine for all of it, and
  • you want every feature to work: the shim, workflows, MCP, header totals and every context-menu action only ever act on the instance they run in.

Start here. Moving to another shape later does not require a reinstall.

One instance, more engines

You can add engines next to race and hoard inside the same process: Nodes tab › This node › + Add an engine (fields Id, Role, Listen port, Interface), or POST /api/engines. Either way Hydranos writes an engine block (TOML array of tables) into default.toml, and the engine starts at the next restart:

[[engine]]
name = "vpn1"
role = "hoard"
engine_id = "vpn1"
  [engine.session]
  listen_port = 16473
  bind_interface = "wg1"

The usual reasons are a separate listen port for part of the catalogue, or a separate network exit for that engine: an interface you brought up yourself (for example a WireGuard interface wg1, Linux only), or a tunnel Hydranos brings up for it in WireGuard, managed by Hydranos mode (Managed WireGuard). The Interface field accepts only an interface whose /sys/class/net/<name>/operstate reads exactly up; tunnel interfaces often report unknown (seen on a tun device; not checked on a wg-quick interface) and are then refused with no interface named … is up. In that case write the block above into default.toml by hand and restart.

flowchart LR
    users["Browser, Sonarr/Radarr, scripts"] -->|"HTTP, port 8199"| api
    subgraph inst["hydranos instance (Linux)"]
        api["HTTP listener"]
        race["engine race<br/>no interface"]
        hoard["engine hoard<br/>no interface"]
        vpn1["engine vpn1, role hoard<br/>bind_interface = wg1"]
        daemon["daemon's own requests<br/>lists, update check, webhooks"]
        api --> race
        api --> hoard
        api --> vpn1
    end
    vpn1 -->|"listener, uTP, peer dials, announces,<br/>DHT, LSD, webseeds"| wg1["wg1 tunnel"]
    race -->|"all its traffic"| defroute["default route"]
    hoard -->|"all its traffic"| defroute
    daemon -->|"[daemon] egress, here the default route"| defroute
Loading

What this costs and what it does not give you:

  • On Linux it is a separate exit for everything the engine does. bind_interface pins every socket of the engine: its listener and uTP, its own outbound dial queue, HTTP and UDP announces, DHT, LSD, webseeds and scrapes. If the interface is missing, the engine reaches nobody rather than falling back to the default route. Two things do not follow it: tracker names are resolved by the host's resolver when the interface is not a tunnel Hydranos created, and the daemon's own requests (tracker lists, IP filter lists, update check, webhooks) leave the way [daemon] egress says. Read VPN and Egress before you rely on it.
  • bind_interface is Linux-only. On Windows and macOS the API and the Network tab refuse it, and an engine whose file still has it stays off the network. Details on Networking Modes.
  • Each engine is a full BitTorrent session (its own peer listener, uTP port, DHT node if enabled, announce scheduler, resume state under <config dir>/<engine id>). Extra engines inherit the [race] or [hoard] settings of their role, then apply their own overrides, except four keys that exist once per host or per decision: listen_port_proxy_v2, gluetun_port_forward, wireguard_enabled and allow_direct. An extra engine gets them only by naming them in its own block. It still inherits the rest, its role's SOCKS5 proxy and bind_interface included, so what the kill switch does with a new extra engine depends on the mode. In WireGuard, managed by Hydranos mode it is kept off the network until it is given a tunnel or Direct (default interface). In the SOCKS5 proxy and SOCKS5 + PROXY-v2 relay modes it is covered if its role has a proxy or an interface (otherwise it needs one, or allow_direct = true). In Gluetun mode it is covered unless it inherits an interface other than gluetun's.
  • Engines are built at boot: adding or removing one takes a restart. With Docker, publish the new engine's port yourself (the shipped compose publishes 16171 and 16172 only).
  • Categories place torrents only in race or hoard. To put one on vpn1, use Move to engine › local-vpn1 (or Duplicate to engine ›) in the context menu, or name the engine through the REST API (engine field of POST /api/torrents/upload, or POST /api/torrents/:info_hash/engine). The context-menu group Node is shown as soon as this node has more than one engine (every default install has race and hoard) or a node is declared; it lists this node's engines as well as declared nodes. See Putting torrents on an extra engine on Engines: Race and Hoard.
  • Visibility is nearly complete: the Race tab lists every race-role engine and the Hoard list every hoard-role engine, the qBittorrent shim lists every local engine's torrents (each hash once), each extra engine has its Engine {id} card on Overview, its curves and live cards on Benchmark, its rows in tracker health and its /metrics series. Still tied to race and hoard: the A / B comparison, the hoard announce cards, Peak live seeds and the race timeline. See Monitoring and Logs and Known Limitations.

Choose this shape when you need a second listen port or a second network exit on the same machine and can drive placement from the context menu or the REST API. Outside Linux, or when the web UI, the shim and the daemon's own requests must sit behind that exit too, run a second instance inside it instead (next section, or Setup 1 on VPN and Egress).

Several instances (nodes)

A node is another complete Hydranos instance that you enrol (+ Enrol a machine) or declare (+ Declare an existing one) on the Nodes tab of the instance you are using, which this wiki calls the controller. The controller stores the node's name, URL and API key in its own store and calls the node's normal HTTP API with that key. The relationship goes one way: the node never calls the controller's API and does not need to know it exists.

flowchart LR
    browser["Browser"] -->|"HTTP, port 8199"| ctl
    subgraph ctlbox["controller instance"]
        ctl["HTTP listener"]
        cr["race"]
        ch["hoard"]
    end
    subgraph nabox["node A instance"]
        na["HTTP listener"]
        nar["race"]
        nah["hoard"]
    end
    subgraph nbbox["node B instance"]
        nb["HTTP listener"]
        nbr["race"]
        nbh["hoard"]
    end
    ctl -->|"HTTP + node A API key"| na
    ctl -->|"HTTP + node B API key"| nb
    ch <-.->|"BitTorrent, only during handoff or fetch"| nah
    cr <-->|"peers"| swarm["BitTorrent peers and trackers"]
    ch <--> swarm
    nar <--> swarm
    nah <--> swarm
    nbr <--> swarm
    nbh <--> swarm
Loading

What you gain:

  • One Hoard list for all machines. The controller's Hoard list merges the rows of every node (a node that does not answer within 4 s contributes nothing to that page; the page is still served). The Location column says where a row lives.
  • Health at a glance. The Nodes tab probes every node live (version, engines, torrent count, status) and Open takes you to a node's own UI, already logged in.
  • Handoff and fetch. Duplicate to engine › / Move to engine › push a local torrent to a node's engine; bring it here pulls one from a node. Only the .torrent travels over HTTP; the data travels over BitTorrent between the two instances' engines and is hash-checked like any download.

What stays per instance:

  • Adding torrents (Add tab, qBittorrent shim, watched folders, MCP) always targets the instance you send it to. Point Sonarr/Radarr at the instance that should hold the data.
  • Categories, workflows, the qBittorrent shim, the Race tab and the header totals are local. The MCP server is half and half: its read tools list the merged rows of every node, its write tools act on local torrents only (MCP Server (AI clients)). A handoff needs the torrent's category to exist on the node when the node has any categories.
  • Most per-torrent actions on a remote row fail from the controller (stop/start, reannounce, recheck, remove, category, tags; set location acts on a local copy). Manage those torrents on the node itself with Open. The full action matrix is on Nodes and Multiple Machines.

Nodes are independent instances. If the controller stops, every node keeps seeding and announcing on its own; if a node stops, the controller's lists just lack its rows until it is back.

Network requirements:

  • The controller must reach each node's URL (port 8199 by default). Enrolment writes a plain http:// URL; keep that traffic on a LAN or a private tunnel.
  • Open redirects your browser to the node's stored URL, so your browser must reach it too.
  • For a handoff, the node dials the controller's engine port, at the address the controller's HTTP request came from; for a fetch, the controller dials the node's engine port. Those engine ports must be reachable between the machines.
  • Enrolment (install.sh) is Linux only. A Windows or macOS instance can still be declared by hand.

Choose this shape when the data sits on different machines, or when you want one instance with a completely different network exit (for example a whole instance inside a VPN), and you can live with managing each node's torrents from its own UI.

One VPN exit per engine

On Linux, one process can give each engine its own VPN exit in 4.4, in either of two ways:

  • WireGuard, managed by Hydranos mode: upload one provider .conf per engine, and Hydranos brings up a tunnel wg-<engine> for each, pins the engine to it and asks the provider for its incoming port where the provider offers one. It needs the NET_ADMIN capability. See Managed WireGuard.
  • Direct mode with one interface per engine that you bring up yourself (bind_interface, see One instance, more engines above).

Either way, all of an engine's peer and tracker traffic follows its own exit. The network mode is one per instance, so gluetun cannot be one engine's exit next to a managed tunnel or a direct engine in the same process: give the gluetun exit its own instance, as below. The other way is one instance per exit. Use it outside Linux, when Hydranos cannot have NET_ADMIN and you cannot bring the interfaces up yourself, or when each exit must also carry its own web UI, shim and daemon requests. Run several Hydranos instances (several containers, or several services on one host), put each one entirely inside its own VPN (Setup 1 on VPN and Egress, for example one gluetun container per instance), and, if you want one merged Hoard list, declare them as nodes on the instance you use as the controller.

flowchart LR
    browser["Browser"] -->|"HTTP"| c1
    subgraph v1["VPN exit 1"]
        c1["instance 1 (controller)<br/>race + hoard"]
    end
    subgraph v2["VPN exit 2"]
        n2["instance 2 (node)<br/>race + hoard"]
    end
    subgraph v3["VPN exit 3"]
        n3["instance 3 (node)<br/>race + hoard"]
    end
    c1 -.->|"optional: declared node, HTTP + API key"| n2
    c1 -.->|"optional: declared node, HTTP + API key"| n3
    c1 --> t1["trackers and peers"]
    n2 --> t1
    n3 --> t1
Loading

Each instance has its own config dir, store, API key and published ports (two instances on one host cannot both publish 8199, 16171 and 16172). The cost adds up per exit: N exits means N qBittorrent shims to configure in Sonarr/Radarr/autobrr, N sets of workflows and categories, N sets of header totals and tracker statistics, and per-torrent actions done in each instance's own UI. The controller's declared nodes do not reach across the tunnels for you: it must be able to reach each node's port 8199, which with a VPN container usually means publishing that port on the host or LAN side.

Remote access

Hydranos serves plain HTTP on port 8199 and has no TLS of its own. To reach it from outside your LAN, put a reverse proxy (or a private VPN) in front of port 8199. Things to know:

  • Create the admin account before you expose anything. Until it exists, the first caller from a loopback, private or link-local address can claim the instance. A public address gets 403 when it reaches Hydranos directly (or through a proxy that itself has a public address), or through a proxy whose X-Forwarded-For starts with the real client address (Hydranos reads the first entry; a proxy that appends to a header the client sent lets the client pick it). A reverse proxy on a private address that sends no X-Forwarded-For makes every caller look local. Details and the full auth model are on Security and Access.
  • Serve Hydranos at the root of its own host name (for example https://hydranos.example.org/). The UI loads /static/… and calls /api/… with absolute paths, so a sub-path such as /hydranos/ does not work.
  • The UI keeps server-sent event streams open (/api/events, and /api/logs/stream for the Logs tab): disable response buffering for those paths in your proxy.
  • Sonarr/Radarr, autobrr and MCP clients use the same listener and the same API key; they can go through the proxy or stay on the LAN address.
  • If the proxy runs on the same host, stop publishing 8199 on all interfaces. With Docker, publish it on loopback only:
    ports:
      - "127.0.0.1:8199:8199"
      - "16171:16171/tcp"
      - "16171:16171/udp"
      - "16172:16172/tcp"
      - "16172:16172/udp"

The engine ports (16171, 16172 and any extra engine's port) are BitTorrent, not HTTP: never put them behind the reverse proxy.

Choosing

Your situation Shape Why
One machine, one network exit One instance Every feature works; nothing to coordinate.
One machine, want a separate listen port or interface for part of the catalogue (Linux) One instance, more engines Same process, same store; placement from the context menu or the REST API. The interface carries all of that engine's traffic.
Everything must leave through a VPN One instance inside the VPN, or (Linux) a managed WireGuard tunnel for each engine A whole instance in the tunnel covers everything, the daemon's own requests included (Setup 1 on VPN and Egress). Per-engine tunnels cover the engines; the kill switch keeps an engine with no tunnel off the network.
Part of the catalogue must leave through a VPN, the rest from home Linux: one instance, an extra engine on the VPN interface or tunnel. Elsewhere: two instances, one inside the VPN, declared as a node Each engine's (or instance's) announces, DHT and dials follow its own exit.
Data on several machines One instance per machine, declared as nodes Data stays where it is; one merged Hoard list; handoff/fetch between machines.
Each engine needs its own VPN exit Linux: one instance, one managed WireGuard tunnel (or interface) per engine. Elsewhere: one instance per exit, optionally declared as nodes See One VPN exit per engine above.
You need Sonarr/Radarr, workflows or MCP actions to cover everything at once One instance (with more engines if needed) The shim and workflows never look across nodes; MCP reads do, MCP writes stay local.

Sizing

All figures below come from the project's own measurement, docs/scale-290k.md: Hydranos 4.2, production, 21–24 September 2026, one machine, about 293,000 torrents plus the race engine, six real trackers. The document calls them "single, sequential runs against live trackers: read them as orders of magnitude, not decimals". The document reports Hydranos 4.2; no 4.3 or 4.4 figures are published, and no measurement of a larger catalogue has been published.

Measure (Hydranos 4.2, 21–24 Sept 2026) Value
Torrents ~293,000 + race
Cold start to API 95–125 s
Resident memory after load 2.8 GiB (about 10 KiB per torrent)
CPU after load 3.4–3.7 cores (hoard + race), while uploading about 5 TB a day
Announces per second 146–155

The same document says that "the figure that matters most at this size is the announce rate": a 293,000-torrent catalogue on a 30-minute interval "needs about 163 announces per second for every torrent to stay visible to its tracker". The measured 146–155 per second is a little below that figure; the document does not say whether some torrents were announced late, and no higher sustained rate has been published. The rate a catalogue needs grows in proportion to its size, so treat anything well above 293,000 torrents on one instance as untested: nothing published says whether memory, cold start or the announce rate scale linearly beyond it.

The README's line "Holding 290,000 torrents takes 2.8 GiB of RAM (about 10 KiB per torrent)" is this same measurement. The README also says Hydranos "runs over a million torrents in a single instance in production today"; no memory, start-up, announce-rate or shutdown figure is published for that size.

A million torrents: not measured. Nothing published gives the memory, cold start, flush time at stop or announce rate of a catalogue that size, of an extra engine, or of a torrent held by two engines. What the code fixes: the announce rate needed is published per engine as hydra_announce_needed_per_second (it grows with the catalogue and shrinks with longer tracker intervals); each engine's scheduler allows at most 4096 announces in flight (2048 per tracker host), so several engines each have their own ceiling; the admission quota spreads the first announces after a start over about 30 minutes; at a clean stop only what leaves in 5 s gets its stopped. Watch hydra_announce_late, hydra_announce_needed_per_second against rate(hydra_announces_total[5m]), and hydra_memory_resident_bytes (Prometheus Metrics) as the catalogue grows.

Practical sizing rules (from the code, not from a benchmark):

  • Worker threads. The runtime uses as many worker threads as the machine has cores, capped at 32. Set HYDRANOS_WORKER_THREADS (an integer above 0) to choose another number, for example on a shared host. See Configuration: Environment Variables, CLI and Signals.
  • Keep the config dir and data dir on a local disk, ideally SSD. The store (hydra.db) and each engine's resume state (state.db) are SQLite files. On a network or FUSE filesystem (NFS, SMB/CIFS, mergerfs, Unraid user shares) Hydranos keeps the store out of WAL mode (rollback journal instead). Torrent data can live anywhere you like. File locations are on Architecture.
  • File handles. The shipped compose and packaging/hydranos.service set the open-files limit to 1048576; the system unit install.sh writes sets 1000000, and its per-user unit sets none (it raises the soft limit to the hard one). Keep a similar limit if you write your own service.
  • Log growth. hydranos.log (next to default.toml) gets one line per incoming peer connection. Hydranos rotates it at 128 MiB and keeps four old files (hydranos.log.1 … .4), so plan about 640 MiB for it; see Monitoring and Logs.
  • Shutdown time. A large catalogue needs time to save resume data on stop; give the service or container a long enough stop timeout. No flush time is published for any catalogue size: read yours in the log after a stop. See Operating Hydranos.

Next step

Hydranos 4.4 · 🇬🇧

Start

Install

Use

Integrate

Network & machines

Operate

Reference


Hydranos 4.4 · 🇫🇷

Démarrer

Installer

Utiliser

Intégrer

Réseau & machines

Exploiter

Référence


Clone this wiki locally