Skip to content

Measurements

How gx-torrent compares with qBittorrent, Transmission, Deluge and anacrolix/torrent, what Gextto costs on a small machine, and how to repeat every number. The figures are the latest ones, not a history: a new round replaces the row it measures.

Short answer

Measured on 2026-11-07 on a low-power mini PC (Intel N97), in Docker, with the same torrents and limits for every client.

Question 1st 2nd 3rd 4th 5th 6th Why it matters
Fastest download with nothing in the way gx-torrent, 285 MB/s Deluge, 253 MB/s qBittorrent, 240 MB/s anacrolix, 197 MB/s rain, 162 MB/s Transmission, 29 MB/s a fast disk and a fast peer
Download on a 100 Mbit/s line, 4 peers, TCP anacrolix, 11.50 MB/s gx-torrent, 11.22 MB/s rain, 11.02 MB/s Deluge, 10.68 MB/s qBittorrent, 10.54 MB/s Transmission, 8.84 MB/s the common case at home
The same line, uTP only Deluge, 11.80 MB/s qBittorrent, 11.56 MB/s anacrolix, 11.50 MB/s gx-torrent, 11.16 MB/s Transmission, 8.25 MB/s rain, no uTP peers that prefer uTP
Smallest peak own memory while downloading at full speed anacrolix, 11 MiB Transmission, 14 MiB rain, 22 MiB gx-torrent, 24 MiB qBittorrent, 26 MiB Deluge, 53 MiB what the client cannot give back to the system
Smallest peak own memory while downloading on a 100 Mbit/s line qBittorrent, 10 MiB anacrolix, 12 MiB Transmission, 13 MiB rain, 30 MiB gx-torrent, 30 MiB Deluge, 39 MiB the same, when the line is the limit
Fastest upload qBittorrent, 285 MB/s gx-torrent, 223 MB/s Deluge, 203 MB/s anacrolix, 120 MB/s rain, 120 MB/s Transmission, 16 MB/s only matters above a gigabit link
Fastest recheck of 4 GiB gx-torrent, 3.8 s Transmission, 3.9 s qBittorrent, 4.3 s Deluge, 4.3 s anacrolix, 6.6 s rain, 10.2 s after a move or a crash
Lowest CPU at rest, nothing loaded rain, 0.01 % anacrolix, 0.01 % Transmission, 0.04 % qBittorrent, 0.05 % gx-torrent, 0.06 % Deluge, 0.28 % a machine that is always on
Lowest CPU at rest, 100 torrents anacrolix, 0.01 % qBittorrent, 0.06 % Transmission, 0.09 % Deluge, 0.31 % gx-torrent, 0.66 % rain, 2.35 % the same, with a long list
Lightest with 100 torrents Transmission, 12 MB RSS rain, 24 MB RSS anacrolix, 25 MB RSS gx-torrent, 29 MB RSS Deluge, 68 MB RSS qBittorrent, 118 MB RSS a long seeding list

The order is the order of the medians: where the ranges of two clients overlap the positions are a tie, and a gap of a few percent is inside the spread of the rounds. The detailed tables below give the ranges. gx-torrent is in bold.

Own memory is the anonymous memory the client really holds, without the file pages that RSS also counts; the 100-torrent row is the only one that uses RSS. The full-speed row is the 4 GiB download from one peer, the 100 Mbit/s row the 512 MiB download from four peers shaped to that line.

In short: gx-torrent is still the fastest over TCP (285 MB/s), the fastest at a recheck (3.8 s) and third on upload (223 MB/s), behind the libtorrent clients. The library row keeps the two rows it took last round: anacrolix/torrent is the lightest at full speed (11 MiB against 24) and the quietest with a hundred torrents (0.01 % of a core), and on the shaped line over TCP it is 2 % ahead — inside the spread, so the two are level; there gx-torrent spends the least CPU per GiB (18.8 s against 21–29). On uTP the libtorrent clients are 3–6 % ahead. rain joins at 162 MB/s and uploads at 120, is level with the best at rest (0.01 %), and pays for its caches: 667 MiB of own memory while uploading and 2.35 % of a core with a hundred torrents. Transmission 3.00 is the lightest with a hundred torrents but seeds 4 GiB at 16 MB/s.

What is best for me

One line per need, with the client that answers it and why. Every figure behind these lines is in this document.

Your need Pick Why
A library, not a downloader: episodes, gaps, renames, upgrades, archive Gextto it decides what to fetch and where it goes; none of the other clients does
One torrent from a browser, on a NAS, a server or a phone, with a qBittorrent-style API gx-torrent, standalone the same engine with its own page and /api/v2, no library to configure
The fastest download over TCP gx-torrent 285 MB/s, against 29–253 for the others
A recheck of a 4 GiB file without a long wait gx-torrent 3.7 s, against 4.0–6.9 for the others
Seeding on a fast LAN (gigabit and above) gx-torrent, qBittorrent or Deluge 244–258 MB/s, three within 6 %, at the lowest CPU per GiB for gx-torrent (2.22 s against 2.38–2.58)
A call or a game while something downloads Gextto, Auto with latency source both the guard leaves room for the other flow and stops taking more as soon as the router sees it
The smallest memory at full speed anacrolix/torrent, 11 MiB against 14–56 the lightest client when the disk is the limit, but its own engine is not a library manager
The quietest client at rest anacrolix/torrent or rain 0.01 % of a core against 0.04–0.28, but rain spends 2.35 % with a hundred torrents and anacrolix 0.01
Ratio farming, a seedbox, no library qBittorrent or Deluge Gextto is a library: it seeds with ratio, day and idle rules and has super-seeding, which lowers the upload by design, but the on-demand share_mode is not planned
Windows gx-torrent, standalone native service, amd64 and arm64; Gextto itself is Unix

In short: a client that downloads and stops there is gx-torrent, the fastest of the six over TCP (285 MB/s against 29–253) and the fastest at a recheck (3.8 s against 3.9–10.2); a client that names, upgrades and keeps the files is Gextto, which is the only one that does it — and carries gx-torrent inside.

Compared with qBittorrent, Transmission, Deluge, rain and anacrolix/torrent

What was run

Every client runs in its own container with the same limits, against the same reference peers, on an isolated network. Each table is the median of three interleaved rounds, with the range in brackets where it matters. Every downloaded file was compared byte by byte with the original.

Client Version Engine
gx-torrent commit 2205ae8d, image fd79b59f gx-core, pure Go
qBittorrent 5.1.0 (Debian 13) libtorrent
Transmission 3.00 (Debian 12) its own, with libutp
Deluge 2.2.0 (Debian 13) libtorrent 2.0.11
rain v2.4.5 (github.com/cenkalti/rain/v2) its own, pure Go
anacrolix/torrent v1.61.0, driven by scripts/compare/anacrolix its own, pure Go
Item Value
Machine Intel N97 (4 cores), 15 GiB RAM, Lexar NM620 NVMe, ext4, Debian 13, kernel 7.1.13, Docker 29.9.3
Client under test CPUs 2–3, 4 GiB of memory limit
Reference peers qBittorrent on CPUs 0–1, 1.5 GiB each, plaintext
Network Docker bridge with no outside access; a private tracker; no DHT, PEX, LSD, UPnP or NAT-PMP
Host ffmpeg stopped, five quiet minutes before the first container; a run with more than 1 core busy outside the test is thrown away and repeated

Two kinds of memory appear below. Own memory (anonymous) is what the client really holds. RSS also counts file pages mapped from disk, which the system takes back whenever it needs them; that is why libtorrent clients and anacrolix show gigabytes on a 4 GiB file without any memory pressure.

At rest

One client, no torrents, 60 s after a 15 s settle.

Client CPU (% of a core) Own memory RSS
rain 0.01 4.4 MiB 16 MiB
anacrolix 0.01 6.5 MiB 20 MiB
Transmission 0.04 1.8 MiB 11 MiB
qBittorrent 0.05 7.1 MiB 35 MiB
gx-torrent 0.06 7.6 MiB 21 MiB
Deluge 0.28 38 MiB 62 MiB

Download at full speed

A 4 GiB file from one reference peer over TCP, with no rate limit and a cold disk cache: the ceiling of each client on this machine.

Client Speed Time CPU per GiB Peak own memory Peak RSS
gx-torrent 285 MB/s (285–290) 15.1 s 3.10 s 24 MiB 37 MiB
Deluge 253 MB/s (250–257) 17.0 s 2.87 s 53 MiB 3,985 MiB
qBittorrent 240 MB/s (237–241) 17.9 s 2.73 s 26 MiB 3,989 MiB
anacrolix 197 MB/s (196–201) 21.8 s 4.84 s 11 MiB 3,403 MiB
rain 162 MB/s (150–162) 26.5 s 4.34 s 22 MiB 34 MiB
Transmission 29 MB/s (29–30) 146.1 s 3.60 s 14 MiB 24 MiB

Transmission 3.00 is four to ten times slower than the others here; the number repeats in every round.

Download on a 100 Mbit/s line

A 512 MiB file from four peers, each shaped to 25 Mbit/s and 40 ms: a line of 12.5 MB/s before protocol overhead.

Client Speed Time CPU (% of a core) CPU per GiB Peak own memory Peak RSS
anacrolix 11.50 MB/s 46.7 s 29 % 27.4 s 12 MiB 524 MiB
gx-torrent 11.22 MB/s 47.8 s 20 % 18.8 s 30 MiB 45 MiB
rain 11.02 MB/s 48.7 s 22 % 21.0 s 30 MiB 41 MiB
Deluge 10.68 MB/s 50.3 s 29 % 29.0 s 39 MiB 575 MiB
qBittorrent 10.54 MB/s 50.9 s 27 % 27.7 s 10 MiB 548 MiB
Transmission 8.84 MB/s 60.7 s 13 % 15.8 s 13 MiB 23 MiB

The first five are within 9 % of each other; gx-torrent and rain use the least CPU per GiB of the four that reach the line.

The same line, uTP only

The same four peers with TCP blocked at the peer.

Client Speed Time CPU (% of a core) CPU per GiB Peak own memory Peak RSS
Deluge 11.80 MB/s 45.5 s 44 % 40.0 s 39 MiB 571 MiB
qBittorrent 11.56 MB/s 46.4 s 41 % 38.5 s 10 MiB 548 MiB
anacrolix 11.50 MB/s 46.7 s 72 % 67.3 s 13 MiB 528 MiB
gx-torrent 11.16 MB/s 48.1 s 39 % 37.1 s 35 MiB 50 MiB
Transmission 8.25 MB/s 65.1 s 19 % 24.0 s 10 MiB 20 MiB
rain no uTP — — — — —

rain has no uTP transport at all, so this scenario has nothing for it to connect to and its row stays empty. The four that reach the line are within 6 % of each other; anacrolix pays twice the CPU per GiB of the libtorrent clients on this path.

Recheck and upload

Both start from the same 4 GiB file with the disk cache dropped: first an explicit recheck, then an upload to one unshaped reference peer.

Client Recheck CPU Upload speed CPU per GiB Peak own memory Peak RSS
gx-torrent 3.8 s (1,138 MB/s) 3.3 s 223 MB/s (204–242) 2.32 s 22 MiB 37 MiB
Transmission 3.9 s (1,098 MB/s) 2.5 s 15.9 MB/s 5.59 s 17 MiB 27 MiB
qBittorrent 4.3 s (1,009 MB/s) 3.3 s 285 MB/s (206–322) 2.48 s 13 MiB 4,105 MiB
Deluge 4.3 s (990 MB/s) 3.3 s 203 MB/s (193–285) 2.96 s 43 MiB 4,098 MiB
anacrolix 6.6 s (648 MB/s) 9.3 s 120 MB/s (119–125) 5.30 s 15 MiB 4,072 MiB
rain 10.2 s (421 MB/s) 3.6 s 120 MB/s (115–125) 4.78 s 667 MiB 679 MiB

gx-torrent and Transmission are level at the recheck (3.8 s against 3.9), ahead of the others. On upload qBittorrent and gx-torrent are within 4 % and Deluge is 9 % behind them; rain and anacrolix are at half the speed, and rain pays for it with its caches: 667 MiB of own memory against 13–43 for the others. Transmission 3.00 sends 4 GiB in four and a half minutes.

100 torrents

A hundred torrents of 2 MiB each, with their files already in place: time to add them, time until all are seeding, then 60 s at rest and the time to come back after a restart.

Client Add All seeding Restart CPU at rest Own memory RSS
anacrolix 1.0 s 1.1 s 0.5 s 0.01 % 10 MiB 25 MiB
qBittorrent 0.9 s 100.4 s 0.5 s 0.06 % 90 MiB 118 MiB
Transmission 0.9 s 11.7 s 0.6 s 0.09 % 2.9 MiB 12 MiB
Deluge 2.6 s 2.6 s 1.0 s 0.31 % 42 MiB 68 MiB
gx-torrent 1.5 s 2.1 s 0.5 s 0.66 % 14 MiB 29 MiB
rain 1.5 s 2.5 s 2.3 s 2.35 % 12 MiB 24 MiB

Transmission reaches seeding at once because it trusts existing files until asked to recheck; anacrolix and rain hash them, but a hundred files of 2 MiB cost a second or two. qBittorrent takes a hundred seconds to verify them. rain spends 2.35 % of a core at rest with these torrents — four times gx-torrent — and takes 2.3 s to restart.

Why the anacrolix row is so light

The library's row is a different trade, not a better engine: it holds almost nothing, and it pays for that in CPU. A few choices explain the 11 MiB of own memory at full speed against gx-torrent's 24, and the 0.01 % of a core at rest against 0.06:

  • Writes go into a file mapping. storage.NewFile (client.go) serves the pieces through storage/file-io-mmap.go: a received block is copied into the shared mapping, with no staging buffer and no write cache. gx-core writes through a buffer taken from a pool, keeps a write cache (1 GiB target) and a read cache (256 MiB default, 8 MiB floor in the daemon). That is more memory, and it is why gx-torrent uploads at 223 MB/s and 2.32 s of CPU per GiB against 120 MB/s and 5.30.
  • No read cache. An upload reads back from the mapping, so the client reserves nothing for it; the mapped pages show in RSS (3.2 GB on a 4 GiB file) and not in own memory.
  • One cap on unverified bytes. MaxUnverifiedBytes (64 MiB, config.go) limits the data received and not yet hashed and written, for the whole client. gx-core bounds a piece and a torrent, not the total.
  • Small units. 16 KiB chunks (global.go) and one buffer per request (peerconn.go), against the 128 KiB blocks of our read cache and 1–2 MiB pieces.
  • Fewer wakeups. No statistics tick, no automatic speed probe, no DLNA, LSD or SSE keepalive.

What gx-core keeps in exchange: the fastest download (3.13 s of CPU per GiB against 4.96), the cheapest upload (223 MB/s against 120) and the cheaper uTP path (37 s per GiB against 67).

What these numbers do not say

  • No ranking on a real swarm. These runs use few peers, no encryption from the peers and no discovery. Behaviour among hundreds of peers, choking included, is not measured here.
  • Settings are defaults, with three exceptions the setup needs: discovery off everywhere, a fixed peer port, and Transmission's uTP off in the TCP scenarios (it otherwise waits on uTP against a TCP-only peer).
  • Transmission is 3.00 from Debian 12, not the 4.1.0 beta of Debian 13: that beta receives the peers from the tracker but never opens a uTP connection to them, so its uTP row was always empty. The pinned image is in scripts/compare/Dockerfile.
  • anacrolix/torrent is a library, not a client: its row is the library with its own defaults, driven by the small HTTP front end in scripts/compare/anacrolix that adds, rechecks and reports the torrents. It has no page, no queue and no library.
  • Small differences are not results. A gap inside the ranges above means nothing.
  • Deluge's web front end runs in a separate container, so its CPU and memory are not counted.

How to repeat the comparison

make measure-compare     # about three hours; the machine must be otherwise idle
python3 scripts/compare/run.py report --tag <tag>

The rule is in AGENTS.md, Build and test: same torrents (generated from a fixed formula, info hashes pinned in scripts/compare/run.py), same pinned images (scripts/compare/Dockerfile), same limits, quiet host, three interleaved rounds. scripts/compare/nightly.sh is the unattended version: it waits for the machine to be quiet, smoke-tests the clients it does not trust yet, measures HEAD in a temporary worktree when the checkout has uncommitted changes, writes the tables and cleans up. Results are written to gextto-data/bench/compare.jsonl. Changing the machine, a version, a limit or a payload starts a new baseline: run every client again and replace every table above.

Gextto and gx-torrent on their own

Same N97 machine, outside Docker unless stated. Every figure is the latest round on this machine.

Footprint

What At rest Under load
gx-torrent 1.1.331, live (DHT on, full IP filter, 3 seeds) 0.3–0.5 % of a core, 28–30 MB RSS (16–18 MB anon) 18–37 % of a core at 8.2–8.8 MB/s with 116–132 peers, 2.0–4.3 s of CPU per 100 MB
gx-torrent, 10 torrents to a NAS about 507 MB outside the two caches, 1.1 GB peak with the NFS write cache
gexttod 1.2.2260, same host 0.4 % of a core, 45 MB anon (70 MB RSS) just started; 97 MB anon (123 MB RSS) after a full search peak 168 MB anon during a full cycle of 47,342 releases (series and movies), about 69 s of CPU, and back to the figure at rest; a series-only cycle of ~38,800 releases peaks at 149–157 MB anon and leaves 85 MB

At-rest figures: three samples of 2, 5 and 10 minutes for gx-torrent (DHT on, full IP filter, three seeds, nothing transferring) and 90 s for gexttod with the integrity check off and no transfer. The gexttod row is the latest full cycle and the latest series-only cycle, with GOGC=50, transparent huge pages off for the process and GODEBUG=madvdontneed=1.

  • After a burst the daemon returns its piece buffers: with 7 torrents and no transfer the RSS falls from 550 MB to 35–40 MB.
  • The free heap comes back to the OS. The same series cycle before the change peaked at 237 MB anon and left 150–190 MB resident for hours; now it peaks at 149–157 MB anon and settles at 85 MB anon (168 MB and 97 MB for a full cycle of 47,342 releases). The explicit release after a cycle or a completed transfer returns 12–92 MB per call, and the 15-minute housekeeping call releases what the background workers free afterwards. Go does not compact, so the spans still pinned by live objects stay.
  • A cycle allocates 1.37–1.43 GB instead of 2.01 GB (~38,800 releases; the spread is the varying size of the feed and indexer replies). The deduplication, the blocklist and the kind filter work in place, the combined feed and indexer batches are sized once, the XML reader no longer translates namespaces, the Torznab error check skips a body without the word, and the indexarr pages share one reused buffer instead of allocating a new one per page. What remains is the XML tokenizer inside encoding/xml (about 170 MB of internal copies per cycle; 7 % of the CPU) and the HTML parser.
  • Archive writes and gap queries are now a small part of a cycle: the identities of the releases just read are probed against the archive's two unique indexes and their rows skipped, and the priority gap pass searches a season once instead of once per episode. A series cycle of ~38,900 releases fell from 39–43 s of CPU to 29 s on the same host The probe covers the batch, not the archive: 1,000 releases a large archive already holds cost 9.3 ms against 19.3 ms with no probe at all, where reading the whole archive first cost 65 ms and grew with it (50,000 rows).
  • systemctl status shows a large number that is mostly page cache, not the process; anon is the real figure (85 MB after a series search, 97 MB after a full one on A MemoryMax on the unit would count the file pages, so it must leave room for them.
  • At rest gexttod opens no directory and writes nothing to the NAS, so its disks can spin down.
  • Where the CPU goes under load (five public ISOs): about 50 % syscalls and scheduler wake-ups, 9–10 % MSE encryption, 5–7 % piece hashing, 2–3 % TLS of web seeds, 9 % the piece picker with about 200 peers.

Name parsing

The quality/language reader runs on every release name and on every filename the library touches. On this machine, BenchmarkParseQuality over five typical names: 95 µs against 696 µs before the word, separator and episode scans replaced the regexes (7.3×), and 15.0 KB / 208 allocations against 21.6 KB / 361. The release-title matchers skip the ten anchored regexes for a name with no season marker, and the SxxExx, date and pack shapes are read without them.

In a cycle the regexp share of the CPU fell from about 39 % to 11 % (profiles of ParseQuality itself is now about 2 % of the sampled cycle. SQLite and the feed/HTML parsing are the larger costs.

The web page

The gx-torrent main page, rendered in German on this machine: 0.78 ms and 141 KB allocated per page, 0.01 ms per small fragment, with the translations already in place. Translating the finished HTML afterwards cost about 2 ms and a tokenizer pass per page.

Peer cap

CPU follows established peers (about 0.1 % of a core each) far more than bytes, when the line is the limit. The daemon sizes the cap from the machine: 55 per torrent and 300 in all on this host. With nine public torrents, two interleaved rounds: 36 % fewer peers and 12 % less CPU than a fixed 55 per torrent, for 4 % fewer bytes, inside the ±10 % spread between rounds.

Real swarms and the home line

What Result Command
Ubuntu ISO on an FTTC 100/15 line 9.3–9.4 MiB/s, libtorrent 6.3–7.1 MiB/s on the same swarm; one campaign, so not a ranking make measure-realswarm
Sharing the line (latency guard) alone 8,503 KiB/s kept; another device downloading gets 55–66 % more TestMeasureLatencyGuard
Seeding a fresh Ubuntu release (137 seeds, 8 leechers), inbound port closed 0 bytes uploaded by gx-torrent and qBittorrent, 4 alternated rounds of 3 minutes; qBittorrent held 6–11 leechers, gx-torrent (10 seeding dials) 0–1 by hand: scripts/gx-core-test.sh next to qBittorrent 5.2.3
Public DHT, 5 minutes 389 nodes, 35.1 % of queries answered, no concentrated subnet make measure-dht
Real libtorrent leecher, loopback the whole 50 MiB file in 4.8 s make measure-choking
Request queue at 50 ms RTT 303 KiB/s with one block in flight, 6,743 KiB/s with the default queue make measure-pipeline
Local swarm, 3 leechers 3.00× the file uploaded; with leechers meshed the seed drops to 1.84× make measure-seeding
Indexers, 30 titles 110 s, 205 requests, 0 failed, 5,935 releases TestMeasureManagerLanes

The real-swarm figure comes from one campaign.

Reusing local data

A torrent sharing files with one already on disk fetches only what is missing. Real Debian (756 MiB) and Arch (1.5 GiB) ISOs, source 75 % complete, one source piece in four corrupted: 25.0 % from the network, every piece verified, in 1.31 s and 2.71 s. The result is the same after a restart, when the sibling is added later, and with a real libtorrent seeder. With nothing to reuse the filter costs 265 ns over 32 torrents. make measure-merge3.

Many files in one torrent

Over loopback from a second daemon (TestMeasureManyFilesDownload), CPU and memory of both daemons together:

Files Size Download Average CPU Peak RSS
1,000 0.15 GiB 1.9 s 85 MB/s 0.65 core 100 MiB
6,000 0.92 GiB 7.9 s 125 MB/s 0.88 core 114 MiB
6,500 3.72 GiB 23.8 s 168 MB/s 1.37 cores 121 MiB

Each file costs one descriptor while the torrent runs, none after a stop. Checking 20,000 small files on disk takes 406 ms.

Synology DS420j (a trial, not a supported target)

Realtek RTD1296 (4 × Cortex-A53), 987 MB RAM, HDD RAID, about 370 MB available before the trial, GOMEMLIMIT=250MiB.

Phase gexttod RSS gx-torrent RSS Lowest available memory Peak iowait
Idle, IP filter loaded 37–66 MB 20–21 MB 318 MB 7 %
Search of 4 titles, 1,703 releases 66 MB 129 MB 240 MB 22 %
6 large torrents, 2 minutes 37 MB 209 MB 160 MB 48 %
4 torrents, 8 minutes, 8–9 MB/s 38 MB 90 MB 290 MB 28 %

Seeding, post-processing and several days of operation were not measured.

The numbers behind engine decisions

A selection of the Go benchmarks: make bench-all runs all of them (six runs, GOMAXPROCS=2) and writes the raw log and a benchstat table to gextto-data/bench/. Round Compare numbers only within one round: the absolute values follow the machine's state more than the code.

Path Cost What it means
Disk write, buffered / mmap (BenchmarkStorageWrite…) 5.5–5.6 / 7.0–7.2 GB/s far above any network speed; mmap is opt-in
16 KiB writes on NFS, batched durability 81 MiB/s (35 without) one fsync per piece removed
Recheck of 4 GiB, cold, gx-torrent 1.1.296 3.8–4.0 s, about 4 s of CPU reading and hashing side by side, read-ahead on
Upload to one peer, cold, gx-torrent 1.1.296 180–240 MB/s, 3.2 s of CPU per GiB read-ahead on, next block loaded in the background
uTP download on a 100 Mbit/s line, gx-torrent 1.1.296 11.2 MB/s, 48 s of CPU per GiB state packets batched over 2 ms
Piece check, v1 / v2, 4 MiB 2.6 / 2.1 GB/s one hash per piece
uTP over loopback (BenchmarkTransferLoopback) 178–188 MB/s single connection
uTP over loopback, 16 KiB writes 11.2 µs of CPU per MB (3.7 s of system time for ~730 MB); throughput 126 MB/s one segmented sendmsg per write (UDP GSO); the wall time is round-trip bound
Picker, rarest pick with 150 peers 8.6 µs one pick in a dense swarm
Peer departure, 27,250 pieces 94–105 µs touches only the pieces the peer held
Refill of 250 requests over TCP 248–256 µs, about 5 writes instead of 250 small messages batched
Read cache hit 355–393 ns, no allocation every uploaded block
Read cache miss, 16 KiB block 675 ns and 168 B the buffer of an evicted block is read into again; the remaining bytes are the benchmark's own key
Webseed, 8 files, 200 ms per request, 4 ranges 0.8 s one host serves four ranges at once; GX_MEASURE=1 go test ./cmd/gx-torrent/ -run MeasureWebseed -v
state.json save, 100 / 1,000 / 5,000 torrents 0.5 / 4.3–5.0 / 20–25 ms held under the daemon lock
IP filter load, 172,000 rules 30–36 ms, 6.6 MB at start
Blocked-IP lookup 20 ns every incoming address
Archive insert, 1,000 releases 43–44 ms per feed batch
Archive search, 100,000 releases 7.9–8.1 ms standalone search
Re-score of 2,000 torrents 55–57 ms one transaction per 500 rows
Series match, 500 series 122–138 µs one release against the list
Prometheus scrape 194–204 µs only when scraped

Seed mode. A torrent added in seed mode hashes a piece when a peer first asks for it. The first block waits 0.19 ms at 256 KiB, 0.68 ms at 1 MiB, 3.4 ms at 4 MiB and 12.8 ms at 16 MiB per piece (file in page cache; a cold read adds one piece of disk time). At most four checks run at once per torrent.

Build, tests and CI

What Time
make build from a cold Go cache 42.5 s
make fast, one package changed / nothing changed 7.1 s / 1.1 s
make gx-torrent 0.5 s
make test, root package in 4 processes 22–24 s (35 s with the test binary rebuilt, 15 s warm)
make test-serial 34 s
go test -count=1 ./... from a cold cache 117 s
make test-ui (Playwright, Chromium) 63–72 s
CI (ci.yml / continuous.yml / codeql.yml), 2 vCPU runners 411 s / 278 s / 210 s

The suite has 3,010 Go tests in 72 packages, 79 Playwright tests and 89 Go benchmarks (count build times, test times and CI times of the same rounds).

Verified without a benchmark

  • Accessibility. 14 axe-core tests pass and the gx-torrent pages had 0 violations in light and dark themes. Method in ACCESSIBILITY.md.
  • Backup and restore. Round trip on a copy of the live data: all 40 tables of the four databases identical, integrity ok. A missing download or temp folder after a restore is a warning you can fix from the interface.
  • eD2k. The EC client passes against a real amuled 2.3.3, and aMule 3.1.0 in Docker completed a download started from the page. Two samples of 30 series put real fallback coverage at 7–13 % with at least 3 sources, too low to justify an own transfer engine: Kad stays a manual search, ed2k_fallback stays off. Reopen on a much larger sample or a material change in metadata.
  • Gap fill on eD2k. A series with 37 gaps across two seasons needed 2 season queries of about a minute each, returning 35 usable copies; one query per episode would have been 37 waits.
  • Upgrade decisions on the live library. 0 of 1,861 archived episodes differ from a rescore or would re-download themselves. 259 feed releases replayed in a dry run took 1.46 s and would replace 2 episodes. ffprobe on a 1.9 GB file takes 274 ms cold, 81 ms warm.
  • Media integrity. The full decode runs near real time (62 minutes of 4K in about 73 minutes); the quick 4K check reads the file and decodes only keyframes, 4 m 09 s for a 62.5-minute 5 GB episode, no false errors, and it can miss a small defect between two keyframes. A later pass costs one stat per unchanged file.
  • Robustness. Lenient Torznab decoding costs nothing measurable, a short uTP header is refused instead of panicking, and the mmap fault guard turns a fault into an I/O error at the same write cost.

Closed designs

Closed with a measurement, so they are not retried without new data.

Idea Why it was closed Reopen when
A bounded list instead of sync.Pool for piece buffers no change in the harness peak (26–28 MiB at full speed either way): three buffers in use with one peer, the pool held nothing idle that mattered
A memory limit or a lower GOGC while downloading at the download peak the next collection target is within 10 % of the live heap (40 against 36 MiB on the line): the memory is live piece buffers, not headroom
io_uring / AIO the disk does 5.5–7.2 GB/s, far above any network; needs cgo or a dependency storage slower than the network
Write-back cache in the engine the storage keeps up with the download storage slower than the network
Batched uTP datagrams (recvmmsg/sendmmsg) 8.3 % of samples on real swarms; saves under 1 % of a core at the live rate a uTP-only session above 8 MB/s
Coalescing contiguous writes the page cache already merges them
Rarity buckets, pickFileEdge cursor no measurement justified the cost
PieceDownloader bitmask 18 KB and 15 allocations per piece: about 450 KB/s of garbage at 100 MB/s
Writing through a file mapping (-mmap, the library's way) same own memory at full speed (24.3 MiB against 24.2) with 12 % more CPU per GiB (3.28 s against 2.93), and with 100 torrents own memory rises (13.4 against 11.7); only RSS moves, to 3.9 GB of mapped pages a storage where a write syscall costs more than a page fault
Skipping the latency probe when nothing transfers the at-rest figure does not move (0.05 % against 0.06, inside the 0.03–0.06 range): the one-second probe is not what the idle CPU is made of a profile showing where the idle wakeups go
Async state.json 4 ms every few minutes at 1,000 torrents, 0.003 % of a core thousands of torrents with frequent changes, or a visible UI stall
Lock-free read cache the lock costs about 0.2 % of a core at 100 MB/s of upload
One PEX loop per torrent 0.0025 % of a core and 2–3 MB at 500 peers
Multi-path get_peers peers_confirmed_multi is 0 in real runs
Anti-Sybil cap on by default the table is not concentrated, and CGNAT subnets would be capped
Peer turnover (closing the slowest peers at the cap) on the Ubuntu ISO swarm the engine never filled the cap: peaks of 13–17 peers at 20, 5–6 at 8, against a default of 55, so turnover never acted a real swarm that keeps a torrent at its cap
Auto cutting the caps when the router sees another device a call (5 pings/s) next to a download filling the line: median 15 ms idle, 36–50 ms loaded, 2–6 of 300 lost; the spikes above 150 ms were there with Gextto idle too, from another device on the line a call that degrades next to a download
Throttling TCP while uTP peers share the line (libtorrent's mixed mode) Ubuntu 26.10, 3-minute rounds: mixed 5.9 and 7.7 MB/s, TCP only 3.9 and 6.7, uTP only 2.0 and 3.4; in the clean round mixed also had the lowest p90/p99 ping (67/163 ms against 79/850 for TCP only) a mixed swarm where mixed loses speed or latency against one transport
Choking changes reciprocation works locally and against libtorrent a gap on a real swarm
Super-seeding by default no upload saved on measurable swarms, about 24 % slower locally
Merge candidates ordered by reusable bytes superseded by the dedup
Upsert with two conflict targets (archive) 17 ms more per 1,000 releases
idx_archive_title, index on lower(magnet_hash) never used by the queries
Latency guard with a fixed ceiling or floor locked to a slow sample or an arbitrary number
A third-party DHT library not a measurement but a model: ours shares the UDP socket with uTP and serves every torrent from one channel

Postponed: qBittorrent's renameFile/renameFolder (needs rename support in the engine) and its setAutoManagement tuning.

Adding a measurement

  • Compare clients only with make measure-compare, and replace all the tables.
  • Compare two versions of our code with -count and benchstat, or interleaved runs. A single pass on a public swarm says a feature works, not which value is better.
  • Measure the system as it really runs, on a quiet host, with queues and auto-management on.
  • Write the machine, date and command next to a number, and replace the old row.
  • Keep this page readable. A number goes here only if a reader can decide something from it; the rest stays in gextto-data/bench/ and in the command that produces it. A rejected idea goes under Closed designs.

The other harnesses are in the Makefile (measure-*, bench-all, bench-all-docker) and in scripts/measure-baseline.sh. Profilers are opt-in on loopback: GX_TORRENT_PPROF=127.0.0.1:6061 and GEXTTO_PPROF=127.0.0.1:6062.