BitTorrent client

Haul

A self-hosted BitTorrent client built on anacrolix/torrent. Accurate ETAs from a short exponential moving average, four-level stall detection that flags dead torrents most clients call healthy, rename-on-complete using media metadata from Pilot or Prism, and VPN awareness out of the box.

Port :8484 Go 1.25 Postgres anacrolix/torrent MIT

What Haul does

Accurate ETAs

Rates and time-remaining are computed from a short exponential moving average (5-second time constant) rather than cumulative totals. Numbers track reality instead of flickering. Gaps over 30 seconds reset the tracker to avoid stale extrapolation.

Multi-level stall detection

Stalled torrents are graded across four severity levels and tagged with a reason — no_peers_ever (pre-metadata, never saw a peer), no_peers (had peers, lost them all), no_seeders (peers but no seeds), no_data_received. Published to /api/v1/stalls for Pilot's stallwatcher to blocklist automatically. The 847-seeder, five-year-old release with zero actual peers is the one this catches.

Rename-on-complete

When Pilot or Prism grab a torrent and pass through media metadata, Haul renames the output into Show/Season 02/Show - S02E05.mkv format automatically on completion. No post-processing scripts required.

VPN awareness

Haul detects whether it's running inside a VPN tunnel and surfaces the external IP in the dashboard — useful for catching VPN drops before they affect your downloads.

Modern React UI

Live-updated over WebSocket — no polling, no stale progress bars. Categories and tags with per-category save paths. Sequential download mode for media players that want to stream before the torrent finishes.

Webhooks and REST API

Webhooks filtered by event type (added, completed, stalled, speed update). Full REST API at /api/v1/ with OpenAPI docs at /api/docs. WebSocket event stream at /ws. Per-torrent and global rate limits.

Screenshots

React 19 + TypeScript UI embedded in the Go binary. Live queue updates over WebSocket.

Haul dashboard — twelve torrents with status filter pills, sort, search, and per-row size, peers, ratio, and progress
Dashboard — status pills, per-torrent stats, live progress bars
Haul torrent detail — status, size, downloaded, peers, ratio, piece map visualization, trackers, paths, and file list
Torrent detail — piece map, trackers, files, and paths

Advanced features

Rate tracker

anacrolix/torrent exposes cumulative byte counters, not rates. Haul samples those counters on each API request and pushes them through an exponential moving average with a 5-second time constant. The math lives in internal/core/torrent/session.go.

Stall classification

Four-level state machine in internal/core/torrent/stall.go. Level 1: no activity for stall_timeout (reannounce). Level 2: 2× the timeout (force DHT). Level 3: 5× the timeout (notify ecosystem). And the special StallNoPeersEver level for torrents that never saw a single peer past the startup grace period — the classic "looks healthy on paper, no one home" case.

Pilot / Prism integration

Pilot and Prism POST to /api/v1/torrents when they grab a release, passing through media metadata so Haul can rename on completion. Haul fires webhooks on completion and publishes stall events to /api/v1/stalls, which Pilot polls to blocklist dead torrents automatically.

Per-category save paths

Categories support path templates with metadata variables. When a torrent arrives with media metadata from Pilot or Prism, the category's path template is expanded before the download starts. Combine with rename-on-complete for fully automatic library filing.

Getting started with Haul

Haul is designed to run behind a VPN container. See beacon-stack/deploy for the full compose setup with VPN.

docker
docker run -d \
  --name haul \
  -p 8484:8484 \
  -v /path/to/config:/config \
  -v /path/to/downloads:/downloads \
  ghcr.io/beacon-stack/haul:latest

Open http://localhost:8484. Haul generates an API key on first run and persists it to /config/config.yaml; it isn't surfaced in the UI. In the full Beacon stack, Pulse and Haul exchange keys automatically through the registration handshake, so you only need to read the config file directly when talking to Haul from outside.

Environment variables

VariableDefaultDescription
DOWNLOADS_PATHRequired. Host path to active downloads. Bound into Haul at /downloads and shared across Pilot, Prism, and Haul.
HAUL_PORT8484Host port for the web UI and API. Set in .env if 8484 is taken.
HAUL_TORRENT_PORT6881Host port for peer-wire (TCP and UDP). Forward this through your router for incoming peers.
HAUL_CONFIG_PATH./config/haulWhere Haul's config.yaml and cached state live.
VPN_USERNAME / VPN_PASSWORDRequired when the VPN overlay is loaded (docker-compose.vpn.yml). Haul tunnels through Gluetun.
LOG_LEVELinfoSet once in .env; Compose passes the same value to every Beacon service. debug, info, warn, error.
TZUTCIANA timezone (e.g. America/New_York). Set once in .env and applied to every service for log timestamps and scheduled tasks.