On this page
Tsunami
High-performance BitTorrent tracker — UDP-first (BEP 15), with an HTTP tracker plane (BEP 3/12) and a JSON stats API for website integration.
Designed for very large swarms (50M+ concurrent peers). Peer storage is a
dense Vec + hash index (~45 bytes/peer measured), sharded by info hash,
with io_uring multishot UDP I/O and SO_REUSEPORT workers.
Features
- UDP tracker (BEP 15): connect / announce / scrape, IPv4 + IPv6.
Two RX backends (
network.rx_backend):io_uring(default) — provided-buffer-ringrecvmsgmultishot + pooledsendmsg.af_packet—PACKET_RX_RINGtap with kernel hash-fanout; parses L2/L3/L4 in userspace and replies over UDP sockets. Trades ~1.5× CPU for much lower burst loss (the kernel ring absorbs floods that overflow UDP socket buffers). NeedsCAP_NET_RAW; interface auto-detected from the default route, override withTSUNAMI_PACKET_IFACE.
- HTTP tracker (BEP 3 announce, BEP 12 scrape),
compact=1andcompact=0(peer_id echo when enabled). - WebSocket tracker (
ws://host:port/announce,/scrape,/connect) — BEP-15 binary messages inside WebSocket binary frames, for WebTorrent and browser-based peers. - Raw BEP-15 over HTTP (
POST /bep15, alias/udp) — send the exact binary request a UDP client would send in the POST body and get the same binary BEP-15 reply back (application/octet-stream). Supports connect / announce / scrape, validates connection ids against your source IP, and returns204 No Contentfor protocol-level drops (same as a UDP client seeing no reply). Lets UDP-protocol tooling run over HTTP(S) or through proxies that can't do UDP. - Real client IP behind a proxy (
http.proxy_ip_header) — when Tsunami sits behind a reverse proxy / load balancer you control, set this to e.g."X-Forwarded-For"and peers register their true IP instead of the proxy's, for/announce,/scrape,/bep15and WebSocket. - Client-supplied announce IP (
protocol.allow_client_ip) — privacy feature: honor the BEP-15ipfield / BEP-3ip=param as the peer's swarm address so clients can announce a relay/non-source IP. The real connection IP still drives conn-id validation, rate limits and access checks, so stats stay honest. Off by default (swarm-poisoning risk). - Unified store: UDP, HTTP, and WebSocket peers share one swarm store, so stats are consistent across protocols.
- Peer-state snapshot/restore: periodic + on-shutdown dump of the full
swarm to disk; restored on startup so restarts don't cause a re-announce
storm. See
[snapshot]. - Rate limiting / DoS shedding: per-worker token buckets shed announce
and scrape load under flood while keeps the cheap connect/parse path
open. See
[ratelimit]. - Full scrape:
GET /scrapewith noinfo_hashreturns stats for every tracked torrent (BEP-12 format, same as opentracker's fullscrape). Enable withhttp.full_scrape. HTTP only — UDP scrape is per-hash-list by design. - HTML dashboard at
/and/dashboard— self-contained stats page (no CDN) covering every stats endpoint: live cards, full per-protocol counter matrix, SVG time-series graphs (requests/s, peers, bandwidth, per-protocol bandwidth, churn, memory, CPU), system gauges, top torrents (click for peer list), top clients, and raw JSON/Prometheus viewers. Every card, panel and chart can be toggled via the Customize drawer (persisted inlocalStorage). Enable withhttp.dashboard(default on). - Stats API (JSON):
GET /api/stats— totals, per-protocol counters + rates, system gauges (RSS/CPU/swarm-bytes), announce-event breakdown, top clientsGET /api/timeseries?seconds=— per-second samples for graphingGET /api/torrents?limit=&offset=— torrent list sorted by swarm sizeGET /api/torrent/{info_hash_hex}?limit=— per-torrent detail + peers (ip, port, seeder flag, peer_id whenprotocol.store_peer_id). Peer IPs are masked by default (http.mask_peer_ips): IPv4 showsa.xxx.xxx.d, IPv6s0:s1:xxxx:xxxx::s7; tracker protocol responses always carry full IPs since peers need them to connect.GET /api/clients?n=— top clients by announce count (BEP 20 names)
- Prometheus metrics:
GET /metrics— counters, gauges, and per-protocol rates in Prometheus text format (Grafana/alerting ready). - Admin-route protection:
/api/*,/metrics,/dashboard,/can be gated with HTTP Basic auth (http.admin_user/admin_password) and/or a source-IP allowlist (http.admin_allowlist). Public tracker endpoints are never gated. Off by default. - Scrape export: periodic
infohash seeders leechersfile (for mirrors). - Access list: allow/deny info hashes, reload on SIGHUP.
- Passkeys (
[passkeys]table +GET /announce/<passkey>) — private-tracker style access: each key is bound to a list of info hashes (empty = all torrents) and satisfies the access list for them. - Event hooks (
protocol.event_hook_url) — POSTs batched JSON of announce lifecycle events (started/completed/stopped/paused) to an HTTP endpoint, for private-tracker integrations. - BEP-21 paused event —
event=pausedkeeps the peer tracked as incomplete but excludes it from downloader counts; scrape responses include adownloadersfield. - Announce interval jitter (
protocol.announce_jitter_secs) — randomizes the announced interval per response to de-phase announce waves after swarm events. - Anti-amplification: stateless HMAC connection IDs (BEP 15 style).
- mimalloc allocator, lock-light sharded design.
Protocol support
- BEP-3 HTTP announce (compact + non-compact peer lists)
- BEP-15 UDP tracker protocol: connect / announce / scrape, IPv4 + IPv6 (BEP-27)
- BEP-21
event=pausedpartial-seed tracking +downloadersscrape stat - BEP-23 compact peer format
- BEP-48 HTTP scrape, including full scrape
- BEP-20 peer_id client identification in stats
- WebSocket tracker (BEP-15 binary messages in WS frames) for WebTorrent
Build
cargo build --release
# binaries: target/release/tsunami, target/release/tsunami-loadgen
Install & OS tuning (Linux)
sudo scripts/install.sh --tune --with-loadgen # build + install + tune
sudo systemctl enable --now tsunami
Installs the binary, a tsunami service user, /etc/tsunami/tsunami.toml,
systemd units, and /etc/sysctl.d/99-tsunami.conf (socket buffers, RX
backlog, NAPI budgets — see DEPLOYMENT.md). scripts/tune.sh separately
handles CPU governor / THP / optional hugepages, RPS and busy-poll, with
--check, --persist (boot unit) and --set KEY=V. See
DEPLOYMENT.md → "One-command install + OS tuning".
Run
./target/release/tsunami -c tsunami.toml
See tsunami.toml.example for all options. Key knobs for large deployments:
network.socket_workers— one io_uring + socket pair per worker; default = CPUs.network.ring_size— io_uring submission entries per worker. The completion queue and provided RX-buffer ring are sized automatically from it (CQ ≈ 16× entries, clamped 16–64K; RX buffers ≈ 4× entries, clamped 2–16K × 2 KiB each), so a biggerring_sizemostly means more in-flight sends, not proportionally more memory. Tune the CQ directly with theTSUNAMI_CQ_SIZEenv var (power of two) if you need to.network.swarm_shards— power of two; more shards = less lock contention.protocol.store_peer_id— costs ~30–50 B/peer extra; only enable if you needcompact=0peer_id echo or per-peer client detail.protocol.client_stats— per-client announce counters (cheap, striped).
CQ-overflow self-healing (io_uring backend)
If a completion-queue overflow ever drops recv CQEs, the kernel can leak
provided-buffer ids and stall UDP RX. Tsunami detects this via the CQ
overflow counter and automatically rebuilds the RX buffer ring: it cancels
the multishot recvmsgs, drains their terminal completions, unregisters +
re-registers a fresh ring, and resubmits — no restart needed. Watch the
cqe_overflow_total counter / tsunami_cqe_overflow_total metric (and the
udp worker: io_uring completion queue overflow / RX buffer ring rebuilt
log lines) to see it happen. TSUNAMI_DEBUG_REBUILD=1 forces the path once
at startup for testing on new kernels.
50M-peer sizing (rough)
- Peer storage: ~45 B/peer (v4) → ~2.3 GiB; ~80 B/peer with
store_peer_id. - I/O buffers: RX ring is clamped to ≤16K × 2 KiB (≤32 MiB) and the send
pool to ≤8K buffers per worker regardless of
ring_size, so e.g. 8 workers stays ≈ 300 MiB worst-case; the CQ is ≤64K × 16 B (≤1 MiB). - Announce rate needed at 30-min intervals: ~28K/s average — far below the ~500K+/s sustained UDP throughput measured on an 8-core box.
- Recommend:
socket_workers = #cores,ring_size = 4096–8192,socket_recv_buffer = 16–64 MiB,swarm_shards = 256+,store_peer_id = falseunless required. Under overload the kernel sheds excess packets withENOBUFS(counted indropped) rather than losing completions — that's expected backpressure, not a fault.
Load generator
./target/release/tsunami-loadgen --target 127.0.0.1:6881 \
--peers 100000 --seconds 60 --numwant 50 --threads 16 --rate 500000
--rate 0 = unlimited flood; --seeders 0.3 = 30% seeders.
Sockets spread across 127/8 loopback aliases for many distinct peers.
Endpoints
| Path | Protocol | Notes |
|---|---|---|
udp://host:port |
BEP 15 | announce/scrape/connect |
/announce |
BEP 3 | GET, bencoded dict |
/scrape |
BEP 12 | GET, repeated info_hash params; no params = full scrape (if enabled) |
/api/stats |
JSON | website stats snapshot (per-proto rates, system gauges) |
/api/stats.csv |
CSV | tidy group,metric,value export of every scalar stat — for log pipelines |
/api/stats.txt |
text | flat dotted.key=value lines — grep/awk/shell friendly |
/api/timeseries |
JSON | ?seconds= per-second samples for graphs |
/api/timeseries.csv |
CSV | wide per-second samples — history logging |
/api/torrents |
JSON | ?limit=100&offset=0 |
/api/torrent/{hex} |
JSON | ?limit=200 peers |
/api/clients |
JSON | ?n=50 top clients |
/metrics |
Prometheus | text exposition format |
/bep15, /udp |
Raw BEP-15 | POST binary request → binary response |
ws://host:port/announce |
WebSocket | BEP-15 in WS binary frames |
ws://host:port/scrape |
WebSocket | same |
ws://host:port/connect |
WebSocket | same |
Verifying
cargo test
cargo build --release
License
AGPL-3.0-only — see LICENSE. Derivative works, including modified trackers offered as a network service, must release their source under the same license.