The daemon¶
There is one long-running process, the daemon, and everything else is a
thin client that connects to it over a Unix domain socket
($XDG_DATA_HOME/torrnado/daemon.sock by default, usually
~/.local/share/torrnado/daemon.sock).
torrnado(no arguments) attaches the TUI to the daemon. If nothing is listening on the socket, it spawnstorrnado daemonas a detached background process first (logging to$XDG_DATA_HOME/torrnado/daemon.log), waits for it to come up, then attaches. Quitting the TUI (q) does not stop the daemon - your torrents keep running.torrnado daemonruns the engine in the foreground until interrupted (Ctrl-C) or sent SIGTERM. This is what gets spawned in the background automatically, but you can also run it yourself directly if you'd rather manage it with systemd/launchd/tmux than let torrnado manage it.torrnado statusreports whether a daemon owns the socket, andtorrnado stopshuts it down and waits. Both identify it by the lock it holds rather than by process name;statusis the one subcommand that will never start a daemon.torrnado add/remove/pause/resume/recheck/priority/limit/move/listare scriptable passthroughs to a running daemon (spawning one if needed, same as the bare command). Everything you can do in the TUI's command palette, you can also do from a shell script.
To see whether one is running, and to stop it:
torrnado status # is a daemon running, which process, and what is it doing
torrnado stop # ask it to stop, and wait until it has shut down cleanly
torrnado status finds the daemon by the exclusive lock it holds on
<state_dir>/daemon.sock.lock for as long as it runs - the same lock
that stops a second daemon claiming the socket. Whoever holds it is the
daemon for this state directory, whatever the binary happens to be
called, and the kernel releases it however the process dies. That is why
it does not match on a process name: pkill -f "torrnado daemon"
quietly finds nothing when the running copy was installed under another
name, and can just as easily match a daemon belonging to a different
state directory.
torrnado stop sends its request down the socket the daemon is already
listening on, so it reaches the daemon for this state directory and
nothing else - it needs no pid, and it can never signal a process that
merely looks like the daemon. It waits rather than firing and forgetting,
because shutting down is when the session is written: returning earlier
would invite starting a new daemon while the old one is still saving.
What it waits on is that same lock being released, which is exactly the
moment a restart would succeed. --timeout 0 returns immediately if you
would rather not wait.
A daemon still stops on SIGTERM exactly as it always did - that is how a service manager stops it, and none of this changes that.
Stopping is still not something to do by reflex. A daemon that is seeding is doing its job, and leaving it running is the normal state for a machine you have set up to do this.
torrnado status never starts a daemon, which no other subcommand can
claim: the rest dial the socket and spawn one when nothing answers, so
asking them whether a daemon is running would make the answer yes. It
also distinguishes a daemon that is running and answering from one that
is running but too busy to accept a connection - a real state, since a
daemon part-way through a hash check can miss any timeout - because
mistaking the second for "not running" is how someone ends up with two
engines on one data directory.
Getting torrents in without logging in¶
watch_dir points the daemon at a directory and it adds any .torrent
file that appears there. That is the last manual step in an otherwise
unattended box: without it the only ways in are torrnado add over SSH
and the TUI, and with it anything that can write a file can add a
torrent - rsync from your laptop, a synced download folder, a file
manager on a Samba share, a cron job.
A file is only taken once its size has stopped changing, so a .torrent
still being copied over a share is not read halfway through. Once added
it is renamed to <name>.torrent.added, and one that could not be parsed
to .failed - renamed rather than deleted, because the file dropped in
may be the only copy, and the marker is also what stops it being added
again after a restart. Sweep them whenever you like.
The rule for what counts is the same one torrnado add <directory> uses:
.torrent files directly inside, no recursive walk.
Running it unattended¶
The daemon is the whole program; the TUI and the CLI are just clients. So
a headless box that downloads things is torrnado daemon running under
whatever supervises processes there (systemd, launchd, tmux - it makes
no difference to torrnado), driven over SSH.
It comes back after a restart. Every change is written to
<state_dir>/session.json - the torrent list, paused state, save paths,
per-torrent rate limits, per-file priorities, when each was added - next
to a copy of each torrent's metainfo in <state_dir>/torrents/. On start
the daemon re-adds them all, and logs how many came back. Data already on
disk is not re-downloaded or re-verified: the piece-completion database
was always persistent, and this is what tells the daemon which torrents
to look it up for.
A session file that cannot be read is logged and skipped, one bad record
at a time. A server that refuses to boot over a malformed record is worse
than one that comes back with fewer torrents, so it starts either way -
journalctl is where you find out which happened.
It can require a VPN. vpn.required = true holds every transfer while
the machine is not on one, and lets them go again by itself when it comes
back - which is the case a box left running for weeks actually hits, when
the VPN client reconnects at 4am and nobody is watching. It is off by
default. Read what it does and does not cover in
Configuration before relying
on it.
Logs go to stderr as text with timestamps and levels, which is what a service manager wants: journald timestamps and stores it with no further configuration.
time=2026-08-05T13:26:28.488-04:00 level=INFO msg="daemon starting" pid=83154 ...
time=2026-08-05T13:26:28.491-04:00 level=INFO msg="session restored" torrents=2
time=2026-08-05T13:26:28.491-04:00 level=INFO msg="daemon ready" socket=... stream=127.0.0.1:64853
Set log.file to write to a file instead. SIGHUP reopens it, which is
what logrotate needs after renaming the old one away - without that the
daemon goes on writing to a file that no longer has a name, and the disk
never gets the space back.
The torrent library's own output is captured into the same stream at
log.library_level, tagged src=torrent. It is much noisier than
torrnado is - it reports every tracker that misbehaves - which is why
it has its own level rather than sharing log.level. Panics still go
straight to stderr through the Go runtime, whatever log.file says, so
leave stderr attached to something.
Two things to know before deploying:
CGO_ENABLED=0changes the piece-completion database. With cgo it is SQLite (.torrent.dbin the download directory); without it, bbolt (.torrent.bolt.db). Neither reads the other's file, so a data directory populated by one build re-verifies from scratch under the other. Pin the choice deliberately when you build for a server, and keep it pinned.- Previews are loopback-only. The stream server binds
127.0.0.1on an OS-assigned port with a per-session token, sotorrnado previewURLs from a remote daemon are only reachable through an SSH tunnel (ssh -L 8080:127.0.0.1:<port> server). That is the intended design, not an oversight: there is no authentication anywhere in torrnado beyond the filesystem permissions on the socket, so nothing it serves should be reachable from a network.
Running a second daemon alongside the first means giving it its own
daemon_socket and its own state_dir. Only the socket is guarded
against sharing (by a lock file); two daemons pointed at one state
directory will each restore the other's torrents and overwrite the
other's session file.
There is no remote control protocol. The socket is local by construction, and SSH already solves the problem properly.
Deployment recipes have their own pages: systemd and Docker.