io_uring based BitTorrent library built from the ground up to maximize performance on modern Linux kernels and hardware.vortex-bittorrent and is a fully trackerless TUI client demonstrating the capabilities of the library.
The project could be considered in a BETA state. The core of the implementation is fast and stable but there are features missing (for example PEX peer exchange). The implementation is also thouroughly tested with unit, fuzz and integration tests and I have tested it manually on various hardware. With that said, vortex-bittorrent is not yet widely used. I expect there to be some stability issues as the project matures. Bug reports and feedback is welcome!
Minimum Linux Kernel Version: 6.1 since I optimize for modern kernels and hardware.
The goal of the library is to be the fastest BitTorrent implementation on Linux. It's hard to fairly and properly benchmark implementations against each other but anecdotaly vortex-cli is around x3 times faster downloading than transmission-cli (4.0.6) on the same hardware and network conditions. If you have another bittorrent implementation that's faster than vortex it would be considered a bug. I won't claim to be the fastest until I have proper benchmarking and comparision against more implementations.

The comparison was done using following for transmission:
rm -rf ~/.config/transmission && rm -rf ~/Downloads/linuxmint-22-cinnamon-64bit.iso
time transmission-cli -f <script that terminates process> -D linux-mint.torrent
And this for vortex-cli, time was taken from the reported value in the TUI when completed:
rm -rf downloads/ && rm -rf ~/.cache/vortex
vortex-cli -t linux-mint.torrent -d downloads
epoll async model. Vortex not only uses io_uring, but optimizes for performance over kernel compatibility which means the latest and greatest apis can be utilized.Arc is necessary.vortex-cli is a simple TUI client is built as a completely trackerless bittorrent client which means all peer discovery is done via the DHT (distributed hash table). The DHT implementation comes from the excellent mainline crate.
You can easily configure the settings for the cli by modifying the config file that's saved by default in ~/.config/vortex/config.toml. To see the commented available settings in the config example
cargo install --locked vortex-cli
vortex-cli supports magnet links and be registered as your default magnet-link handler by running the register-magnet-handler subcommand.
vortex-cli --magnet-link <magnet-link> -d downloads
A cli for downloading torrents using the bittorrent protocol(s). Built on top of io-uring
Usage: vortex-cli [OPTIONS] <--info-hash <INFO_HASH>|--torrent-file <TORRENT_FILE>|--magnet-link <MAGNET_LINK>>
vortex-cli [OPTIONS] <COMMAND>
Commands:
register-magnet-handler Register vortex-cli as the system-wide magnet link handler
help Print this message or the help of the given subcommand(s)
Options:
-p, --port <PORT>
Port for the listener
-i, --info-hash <INFO_HASH>
Info hash of the torrent you want to download. The metadata will be automatically downloaded in the swarm before download starts
-t, --torrent-file <TORRENT_FILE>
Torrent file containing the metadata of the torrent. if this is provided the initial metadata download will be skipped and the torrent downlaod can start immediately
-m, --magnet-link <MAGNET_LINK>
Magnet link containing the info hash of the torrent. The metadata will be automatically downloaded in the swarm before download starts
-c, --config-file <CONFIG_FILE>
Path to the config file (defaults to $XDG_CONFIG_HOME/vortex/config.toml) the file will created it if doesn't already exists
-d, --download-folder <DOWNLOAD_FOLDER>
Path where the downloaded files should be saved (defaults to $XDG_DATA_HOME/vortex/downloads)
--log-file <LOG_FILE>
Log file path (defaults to $XDG_STATE_HOME/vortex/vortex.log)
--dht-cache <DHT_CACHE>
DHT cache path (defaults to $XDG_CACHE_HOME/vortex/dht_bootstrap_nodes)
--skip-dht-cache
Skip use of the DHT node cache and rebuild from bootstrap nodes
-h, --help
Print help
-V, --version
Print version
f
Currently the following BEPs have been implemented. I have also looked at the excellent libtorrent implementation when BEPs have been underspecified or unclear.
As mentioned earlier, the library uses a custom runtime/event loop instead of any existing async io uring libraries (I've tried multiple) for maximum control over the lower level details of the implementation. The over all implementation is multi-threaded but uses a single io_uring thread for all I/O operations. The other threads are used for calculating piece hashes to avoid blocking the main loop.
Communication with the main torrent thread is done via the Command message. You receive updates of the torrent progress via the TorrentEvent message.
For maximum performance it's key to tweak the config for your specific setup. Also ensure your various ulimit limitations aren't to strict. You may want to raise the limits as well for maximum performance.
See the basic_download integration test for a minimal example of how to use the library.
vortex-bittorrent have support for the metrics crate for thourough observability when using the metrics feature flag. There is an example grafana dashboard I personally use in dashboard.json
| BEP | Title | Status |
|---|---|---|
| 3 | The BitTorrent Protocol Specification | Implemented |
| 6 | Fast Extension | Implemented |
| 7 | IPv6 Tracker Extension | Not Implemented |
| 9 | Extension for Peers to Send Metadata Files | Implemented |
| 10 | Extension Protocol | Implemented |
| 11 | Peer Exchange (PEX) | Not Implemented |
| 12 | Multitracker Metadata Extension | Won't implement now (trackerless) |
| 14 | Local Service Discovery | Not Implemented |
| 19 | WebSeed - HTTP/FTP Seeding (GetRight style) | Not Implemented |
| 20 | Peer ID Conventions | Implemented |
| 21 | Extension for Partial Seeds | Implemented |
| 27 | Private Torrents | Not Implemented |
| 29 | uTorrent Transport Protocol (uTP) | Not implemented |
| 40 | Canonical Peer Priority | Not implemented |
| 52 | The BitTorrent Protocol Specification v2 | Not Implemented |
| 54 | The lt_donthave extension | Not Implemented |
As you may have noticed I have a CLAUDE.md file in the repo and I frequently use LLM myself for bouncing ideas, doing boring work like updating or writing tests and help with some UI code in vortex-cli. The core implementation of vortex-bittorrent is hand written and thouroughly reviewed. To get across acceptable LLM usage in this repo, I've copied over the policy cargo-nextest uses, which I thought was great:
LLMs represent a tremendous breakthrough in software engineering. I welcome LLM-assisted contributions that abide by the following principles:
If your LLM-assisted PR shows signs of not being written with thoughtfulness and care, such as missing cases that human review would have easily caught, I may decline the PR outright.
This project is licensed under the BSD-3-Clause License - see the LICENSE file for details.
24 activities
monsoon does show per-file progress but doesn't yet attempt to remove files from the download queue. i also want to be able to set priority on a per-file basis so that the user can prioritise some file or subset of files for earlier download than others. i think it will require vortex support. i'm tracking that at https://git.lair.cafe/monsoon/monsoon/issues/1 but haven't gotten round to doing anything about it yet.
monday, june 15, 2026 — 16:25:39 utcgood call. done in cf697d9. TorrentProgress now keeps the BitBox as a private field and delegates get()/iteration to bitvec, so the MSB0 logic isn't reimplemented and bitvec stays out of the public api. progress() clones the bitfield directly.
kept it (now State::progress(), returning the same Option<TorrentProgress>), because the metrics event only fires on a tick once the event loop is running. this is the at-rest accessor: a client that resumes an already-complete or partially-downloaded torrent needs the completed set before starting the loop to render correct per-file progress immediately, rather than showing 0% until the first tick. the downstream monsoon client uses it exactly this way at torrent-registration time. happy to drop it if you'd rather not expose at-rest state, but it does cover a case the event alone doesn't.
fixed by the above: report_tick_metrics now calls piece_selector.progress() once and moves it straight into the event, so the clone is gone.
done.
completed_clone() is replaced by progress(), which builds the byte buffer directly via self.completed_pieces.as_raw_slice().to_vec().into_boxed_slice() and returns a TorrentProgress.
no more intermediate BitBox clone.
done.
added a public TorrentProgress type that hides the bitfield behind get(index), total_completed(), num_pieces(), and an iterator (iter() plus impl IntoIterator for &TorrentProgress). pieces_completed is folded in as the return of total_completed(), and the variant now exposes a single progress: Option<TorrentProgress> instead of the three raw fields.
added unit tests for the MSB0/count/iteration semantics.
The piece length in the torrent metadata is 1,986,560 bytes (not a power of 2). That's 121 × 16384 + 4096, so the last subpiece of every non-last piece is only 4,096 bytes.
A peer legitimately requests (begin=1982464, length=4096) — this passes is_valid_piece_req correctly since 1982464 + 4096 ≤ 1986560. The validation in #129 can't reject this request, nor should it.
The bug is in the disk read completion handler. The end_idx computation:
let end_idx = start_idx
+ state.piece_selector.piece_len(piece_idx)
.min(SUBPIECE_SIZE as u32) as usize;
always adds 16,384 bytes from the offset because piece_len.min(SUBPIECE_SIZE) resolves to SUBPIECE_SIZE for any normal-sized piece. When piece_offset + 16384 > piece_length (last subpiece of a piece whose length isn't a multiple of 16384), it overflows the buffer:
start_idx = 1,982,464end_idx = 1,982,464 + 16,384 = 1,998,848buffer.len() = 1,986,560I've updated this PR to fix the root cause — end_idx is now capped at piece_len instead of blindly adding SUBPIECE_SIZE:
let piece_len = state.piece_selector.piece_len(piece_idx) as usize;
let end_idx = (start_idx + SUBPIECE_SIZE as usize).min(piece_len);
This is why you can't reproduce it easily — most torrents use power-of-2 piece lengths (256 KiB, 512 KiB, 1 MiB…) which are always exact multiples of 16,384. It only triggers with non-standard piece lengths.
The buf_pool.rs panicking() guard is kept as defense-in-depth (matching the existing BufferRing::drop pattern from #95).
#129 tested switched monsoon back to vortex upstream. however this reintroduced panic crashes in monsoon. so for monsoon the only option is to remain dependent on a fork or await a merge on this pr or a new patch.
thursday, april 9, 2026 — 16:07:31 utc