Haul
A self-hosted BitTorrent client for home servers and the Beacon media stack.
Website ·
Bug Reports
Haul is a BitTorrent client with a React web UI and a REST API. Run it on its own as a modern qBittorrent / Transmission alternative, or alongside Pulse, Pilot, and Prism — the Beacon Stack — where it picks up rename-on-complete, stall blocklisting, and the rest of the integrated *arr pipeline. It's built on anacrolix/torrent, runs as a single Go binary, stores state in Postgres, and is configured from the UI or through environment variables.
Is this for you?
Haul is built to be approachable by default and capable when you want it to be. The out-of-the-box defaults are tuned so you can docker run it, open the UI, and be downloading inside of a minute — sensible save paths, a working rate tracker, stall detection, and VPN awareness all on from the start. The deeper features are there too: full REST and WebSocket APIs, configurable stall thresholds, per-category save-path templating, webhook event routing, sequential download and piece-priority modes, custom rename formats. They stay out of your way until you turn them on.
You'll probably like Haul if you:
- Run a homelab and want a torrent client with a modern web UI that doesn't look dated
- Use or plan to use Pilot or Prism for TV and movie management
- Want accurate ETAs and reliable dead-torrent handling without manually babysitting grabs
- Appreciate sensible defaults now and the option to grow into advanced features later
Features
- Modern React UI, live-updated over WebSocket — no polling, no stale progress bars
- Accurate ETAs. Rates and time-remaining are computed from a short moving average rather than cumulative totals, so numbers track reality instead of flickering
- Categories and tags with per-category save paths and tag-based filtering
- Sequential download mode for streaming before the torrent finishes
- First-and-last-piece priority for media players that peek at file headers
- Rename-on-complete — when Pilot or Prism grab a torrent and pass through metadata, Haul renames the output into
Show/Season 02/Show - S02E05.mkv format automatically
- Stall detection with three classification levels. Dead torrents (no peers ever, or gone silent past the timeout) are published to
/api/v1/stalls so Pilot's stallwatcher can blocklist them before they waste another retry
- 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 become a problem
- Webhooks filtered by event type (added, completed, stalled, speed update)
- Per-torrent and global rate limits
- Magnet URIs, DHT, PEX, µTP, and crash-safe resume via a persistent piece-completion store
- Full REST API (OpenAPI docs at
/api/docs) and a WebSocket event stream at /ws
- Postgres-backed state for torrents, categories, tags, and settings
- Zero telemetry. No analytics, no crash reporting, no phoning home
Getting started
Standalone
A single-service compose that runs Haul with its own dedicated Postgres lives at docker/docker-compose.yml. Edit the two /path/to/... lines and the placeholder password, then:
docker compose -f docker/docker-compose.yml up -d
The web UI is at http://localhost:8484. Haul generates an API key on first run; find it in Settings → System.
As part of the Beacon Stack
For the full setup — Pulse, Pilot, Prism, FlareSolverr, and Haul behind a VPN container — see beacon-stack/deploy. Standalone Haul works on its own; run it with the stack and rename-on-complete, stall blocklisting, and centralized indexer management light up.
Build from source
Requires Go 1.25+ and Node 22+.
git clone https://github.com/beacon-stack/haul
cd haul
cd web/ui && npm ci && npm run build && cd ../..
make build
./bin/haul
Configuration
Most settings live in the web UI. For the ones you'll want at container-start time, use environment variables or a YAML config file at /config/config.yaml (also searched at ~/.config/haul/config.yaml and ./config.yaml).
| Variable |
Default |
Description |
HAUL_SERVER_PORT |
8484 |
Web UI and API port |
HAUL_TORRENT_LISTEN_PORT |
6881 |
Peer-wire listen port |
HAUL_TORRENT_DOWNLOADS_PATH |
/downloads |
Default save path |
HAUL_DATABASE_DSN |
— |
Postgres DSN (required) |
HAUL_AUTH_API_KEY |
auto |
API key; autogenerated on first run if unset |
HAUL_PULSE_URL |
— |
Pulse control-plane URL (optional) |
HAUL_TORRENT_RENAME_ON_COMPLETE |
false |
Rename completed downloads using media metadata |
HAUL_TORRENT_PAUSE_ON_COMPLETE |
false |
Pause torrents as soon as they finish (for ratio-sensitive trackers) |
HAUL_TORRENT_STALL_TIMEOUT |
120 |
Seconds of inactivity before a torrent is classified as stalled |
Where Haul fits in the Beacon stack
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Pilot │ │ Prism │ │ Pulse │
│ (TV) │ │ (movies) │ │ (control │
│ │ │ │ │ plane) │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ grab torrent │ grab torrent │
▼ ▼ │
┌───────────────────────┐ │
│ Haul │◄─────────────┘
│ (BitTorrent) │ optional:
│ │ stall events, webhooks
└───────────┬───────────┘
│
▼
downloads/
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.
You can run Haul standalone and ignore the rest — the media-manager integration is opt-in via the rename_on_complete setting and the upstream service passing metadata.
Power user notes
A few things worth knowing if you want to go deeper than the UI:
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. Gaps over 30 seconds reset the tracker to avoid extrapolating from stale data. The math lives in internal/core/torrent/session.go — tweak the time constant there if the default feels too slow or too twitchy for your connection.
Stall classification. Three-level state machine in internal/core/torrent/stall.go:
no_peers_ever — torrent has never seen a peer after the grace period
activity_lost — bytes were flowing, but nothing has changed past the stall timeout
complete_but_no_activity — finished but hasn't uploaded anything recently (useful for ratio-enforcing trackers)
Anything above level 1 shows up on /api/v1/stalls.
Regression suite. Haul has been bitten by dead-torrent bugs often enough that there's a locked-in test suite covering the failure modes. make test runs it in under two seconds. If you're editing the session wiring, stall detection, or the rate tracker, the suite will catch regressions before they ship. See CLAUDE.md for the guarded files.
Webhooks. Configure HTTP callbacks filtered by event type. Payloads are the same shape as the WebSocket events, so you can reuse your event handler code.
API surface. The REST API is complete — anything the UI does is available over HTTP. Interactive docs at /api/docs. The Go client lives in pkg/sdk if you want to integrate from another Go service.
Privacy
Haul makes outbound connections only to peers, trackers, and the optional Pulse URL you configure. No telemetry, no analytics, no crash reporting, no update checks. API keys and credentials stay in your local database.
Built with Claude
Haul was built by one person with extensive help from Claude (Anthropic). Architecture, design decisions, bug triage, and this README are mine. Many of the keystrokes are not. If something in the code or the docs doesn't make sense, that's a bug worth reporting — open an issue.
Development
make build # compile to bin/haul
make run # build + run
make dev # hot reload (requires air)
make test # go test ./...
make check # golangci-lint + tsc --noEmit
make sqlc # regenerate sqlc code
Contributing
Bug reports, feature requests, and pull requests are welcome. Please open an issue before starting anything large.
License
MIT — see LICENSE.