A self-hosted BitTorrent client for home servers and the Beacon media stack.
Haul is a BitTorrent client with a React web UI and a REST API. It's designed to drop into a Sonarr/Radarr-style pipeline — specifically Pilot and Prism — but it runs fine on its own as a standalone client. 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.
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
- 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.mkvformat automatically - Stall detection with three classification levels. Dead torrents (no peers ever, or gone silent past the timeout) are published to
/api/v1/stallsso 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
docker run -d \
--name haul \
-p 8484:8484 \
-v /path/to/config:/config \
-v /path/to/downloads:/downloads \
ghcr.io/beacon-stack/haul:latestThe web UI is at http://localhost:8484. Haul generates an API key on first run; find it in Settings → System.
The full Beacon stack — Postgres, Pulse, Pilot, Prism, and Haul behind a VPN container — lives in beacon-stack/deploy. Point it at a media directory and everything's wired up.
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/haulMost 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 |
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 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.
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 periodactivity_lost— bytes were flowing, but nothing has changed past the stall timeoutcomplete_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.
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.
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.
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 codeBug reports, feature requests, and pull requests are welcome. Please open an issue before starting anything large.
MIT — see LICENSE.