TorrentNG is a modern torrent management stack targeting headless power-user seeding at scale (10k–100k torrents, 200+ TB).
It is NOT a ruTorrent cosmetic fork. It now has a native Rust BitTorrent engine rewrite as the primary runtime path, while the rTorrent-backed sidecar remains available for migration, compatibility testing, and users who still want the upstream rTorrent core.
It has two runtime tracks:
- Native rewrite —
crates/torrentngdowns torrent state, peer traffic, tracker state, storage, rechecks, jobs, metrics, native REST/SSE, and compatibility API projections. - Track 1 rTorrent core — rTorrent/libtorrent remains the BitTorrent
engine;
sidecar/torrentngbridges trusted local SCGI/XMLRPC into the WebUI, native REST facade, qBittorrent-compatible API, cache, auth, and metrics.
Important layers:
- engine-profile/ — Pinned rTorrent build config, SCGI/socket setup, tuning profiles
- sidecar/ — Rust daemon for rTorrent-backed deployments
- crates/ — Native engine crates and
torrentngd - webui/ — React+Vite frontend, virtualized table, talks to native/sidecar APIs
- deploy/ — Docker, Compose, systemd, nginx, Kubernetes examples
- External tools (Prowlarr, Sonarr, Radarr, autobrr, cross-seed) talk to TorrentNG through compatibility APIs, primarily the qBittorrent-compatible API.
- The browser talks to native REST/SSE/WebSocket-facing APIs, never directly to rTorrent SCGI.
- In native mode,
torrentngdis the source of truth and does not require rTorrent, XMLRPC, or the sidecar. - In Track 1 sidecar mode, nothing talks to rTorrent XMLRPC/SCGI directly except the sidecar.
- The sidecar runs beside rTorrent, communicates over a trusted local SCGI socket, and remains a migration/facade layer.
- Auth, tokens, CSRF/OIDC/reverse-proxy trust policy live in the TorrentNG API layer.
Track 1 fixed immediate rTorrent/ruTorrent pain, but could not fix engine-level limits:
- rTorrent owns storage behavior and has no TorrentNG userspace disk scheduler.
- Rechecks are not durable TorrentNG jobs with pause/resume/cancel semantics.
- Torrent lifecycle history is limited compared with native structured events.
- The sidecar must poll and translate XMLRPC state.
- Engine behavior depends on rTorrent/libtorrent build details.
- BEP 52/v2, compatibility facades, migration, and metrics are simpler when projected from one native model.
See docs/ENGINE_REWRITE.md for the practical guide and docs/ENGINE.md for
the deeper design.
rTorrent/libTorrent remains a strong baseline for large headless seed libraries:
- Low memory growth over time
- Strong session persistence and resume
- Low churn seeding workload fits its concurrency model
- Existing user deployments need migration and comparison paths
- rTorrent 0.16.9+ introduced trusted/untrusted XMLRPC connection model
- Raw SCGI/httprpc passthrough breaks external clients (
load.startblocked for untrusted connections) - Prowlarr, Sonarr, Radarr, Transdrone, NZB360 all hit this
- xmlrpc-c build path is erratic; tinyxml2 preferred
- ruTorrent 10k+ torrent UI is sluggish (hotfix shipped in v5.2.10)
- No clean daemon/API/event model — everything is PHP polling XMLRPC
crates/torrentngd is daemon-first, but argv[1] dispatches two offline
state tools before the daemon starts (run via spawn_blocking):
torrentngd migrate --source <client> --from <dir> [--apply]— import other clients' state into the native model. Dry-run by default;--applywrites DB rows +rt-fastresumestate and persists.torrentblobs intosession_dir/torrents.--remap OLD=NEW,--policy verify|trust-hints|trust-all.torrentngd export --format <client> --to <dir> [--apply]— reverse migration (anti-lock-in). Reads native state read-only, writes the target client layout.
Both reuse crates/rt-migrate (rt_migrate::export for the reverse path) and
report fidelity buckets. Source code: crates/torrentngd/src/{migrate,export}.rs.
Certification: crates/rt-migrate/tests/round_trip_matrix.rs (all clients ×
import/export/round-trip) plus scripts/migration_corpus_certification.sh.
Entry: sidecar/src/main.rs
Crates: axum 0.7, tokio, serde/serde_json, toml, rusqlite (bundled), tracing, anyhow, quick-xml
Modules:
config— TOML config loading, env override (TNG_*)rtorrent::client— async XMLRPC/SCGI client over Unix socket or TCPrtorrent::torrents—d.multicall2torrent query, CRUD ops,set_user_agentapi::server— axum router, AppStateapi::handlers— native REST handlers includingGET/PUT /api/v1/settings/user-agentapi::ws— WebSocket event broadcastqbcompat— qBittorrent v2 API shimcache::db— rusqlite schema, upsert/delete, WAL modecache::query— server-side filter/sort/paginatesync— background tokio task: rTorrent poll → cache upsert → WS broadcast
API surface:
/api/v1/...— native JSON API/api/v1/settings/user-agent— GET/PUT user-agent (live, pushes to rTorrent)/api/qb/v2/...— qBittorrent-compatible passthrough/ws— WebSocket event stream/health— health check
Entry: webui/src/main.tsx
Key constraints:
- Virtualized torrent table (TanStack Virtual or similar) — must handle 100k rows
- Server-side sort/filter via native or sidecar API — never load all torrents to browser
- No right-click dependency for mobile support
- Delta sync via WebSocket — no full-refresh polling loops
- Settings view includes
UserAgentPanelcomponent for live user-agent management
Must pass *arr/autobrr integration tests:
POST /api/qb/v2/auth/loginGET /api/qb/v2/app/versionGET /api/qb/v2/app/webapiVersionGET /api/qb/v2/torrents/infoPOST /api/qb/v2/torrents/addPOST /api/qb/v2/torrents/pausePOST /api/qb/v2/torrents/resumePOST /api/qb/v2/torrents/deletePOST /api/qb/v2/torrents/recheckPOST /api/qb/v2/torrents/reannounceGET /api/qb/v2/torrents/trackersPOST /api/qb/v2/torrents/editTrackerGET /api/qb/v2/torrents/filesPOST /api/qb/v2/torrents/filePrioPOST /api/qb/v2/torrents/setCategoryPOST /api/qb/v2/torrents/addTagsGET /api/qb/v2/sync/maindataGET /api/qb/v2/transfer/info
Every release must pass:
- 1k torrents: UI first paint < 1s, filter < 100ms
- 10k torrents: UI first paint < 2s, filter < 200ms
- 15k torrents: UI first paint < 3s, filter < 500ms
- 50k synthetic: compatibility API
/torrents/info< 500ms /sync/maindatadelta < 50ms under normal churn- daemon/sidecar memory within release target at 15k torrents after 24h
Track 1 — rTorrent sidecar: fix rTorrent/ruTorrent pain without replacing the engine. Phases 0–5. This remains available for migration and rTorrent-core comparison.
Track 2 — Native Rust engine: ground-up Rust BitTorrent daemon, 10k–100k torrents, 200+ TB, seeding-first. This is now the primary runtime path. See docs/ENGINE_REWRITE.md and docs/ENGINE.md.
- Phase 0: Audit rTorrent 0.16.x + ruTorrent 5.3.x breakages
- Phase 1: Known-good distribution bundle (pinned versions, patched httprpc trust)
- Phase 2: Sidecar daemon MVP (list/add/remove/start/stop/events)
- Phase 3: qBittorrent API compatibility shim
- Phase 4: Modern WebUI
- Phase 5: Plugin/workflow platform
- Research/design lock → 1. Foundation crates (bencode/metainfo/hash/piece-map) → 2. Storage + recheck engine → 3. Tracker engine → 4. TCP seeding MVP → 5. Session daemon → 6. qBit API compat v1 → 7. Downloading → 8. Scale hardening (15k/200TB) → 9. Web UI → 10. DHT/PEX/uTP → 11. BEP 52/v2 → 12. Production 1.0
- Rust daemon/sidecar: axum + tokio; no unsafe except in deps; anyhow for errors in binary, thiserror for library errors
- WebUI: TypeScript strict, TanStack Query for server state, TanStack Virtual for table
- No ORM; raw SQL via rusqlite with bundled SQLite (no system dep)
- Native config file:
TORRENTNGD_CONFIG,~/.config/torrentngd/config.toml, or/etc/torrentngd/config.toml - Sidecar config file:
~/.config/torrentng/config.tomlor/etc/torrentng/config.toml; env varsTNG_*override many sidecar fields - All API responses: JSON, snake_case keys
- Logs: structured JSON via tracing + tracing-subscriber JSON layer
Configurable via [rtorrent] user_agent in config or TNG_USER_AGENT env var.
Default: rtorrent/0.16.11/0.16.11 (used in packaged releases).
Pushed to rTorrent via network.http.user_agent.set on startup.
Runtime update: PUT /api/v1/settings/user-agent or Settings panel in WebUI.
Peer ID family prefix: -lt100B-, fixed. The other 12 bytes are generated
and persisted per install (native: <session_dir>/peer_id_suffix; sidecar:
<data_dir>/peer_id_suffix) — NOT a shared literal. A shared/hardcoded
peer_id suffix got a real user banned from a private tracker (MAM) for
"running multiple instances of the same client," because every install
without one presented the identical peer_id. Only set [rtorrent] peer_id
or TNG_PEER_ID to pin one specific install (e.g. a test fixture) — never
in a shared template, base image, or fleet-wide env var.
Do not strip the user-agent to rtorrent/0.16.11, do not use
rtorrent/0.16.11/000 as a peer ID, and do not guess -lt1011-.
See docs/TRACKER-IDENTITY.md before changing this.