Building a learning ready BitTorrent Client from Scratch in Go
How I implemented the full BitTorrent protocol stack — DHT, magnet links, BEP 9 metadata fetching, adaptive scheduling, and more — in pure Go with zero non-standard dependencies.
Why Build a BitTorrent Client?
Most developers treat BitTorrent as a black box. You drag a .torrent file into qBittorrent, and files appear. But the protocol underneath is a sophisticated distributed system that solves some genuinely hard problems: peer discovery without a central server, content integrity verification, NAT traversal, choking fairness, and tail-completion efficiency.
I decided to build a fully functional, production-ready BitTorrent client in Go — not a toy, not a tutorial snippet, but something that actually downloads real torrents, including Ubuntu ISOs, from the public swarm. The result is a ~3,000-line codebase that implements the core BitTorrent BEPs (BitTorrent Enhancement Proposals) end to end.
What It Supports ?
Before diving into implementation, here’s a summary of what the client can do:
Architecture at a Glance
User input (.torrent file or magnet link)
│
├─ .torrent ──► Bencode decode ──► TorrentInfo (pieces, files, trackers)
│
└─ magnet ───► Parse URI ──────────┐
│
┌───────────────────────▼──────────────────────┐
│ Peer Discovery (in order) │
│ 1. HTTP trackers (BEP 3) │
│ 2. UDP trackers (BEP 15) │
│ 3. DHT bootstrap (BEP 5) — fallback │
└───────────────────────┬──────────────────────┘
│
┌───────────────────────▼──────────────────────┐
│ BEP 10 Extension Handshake (magnet only) │
│ BEP 9 ut_metadata fetch → SHA-1 verify │
└───────────────────────┬──────────────────────┘
│
┌───────────────────────▼──────────────────────┐
│ Download Engine │
│ • Resume state + piece verification │
│ • Rarest-first + endgame scheduling │
│ • Long-lived sessions + pipelined requests │
│ • Adaptive peer scoring / quarantine │
│ • Inbound listener + upload serving │
└───────────────────────┬──────────────────────┘
│
$HOME/Downloads/The project uses a clean internal/ layout with no circular dependencies and only one external library: github.com/jackpal/bencode-go for bencode encoding/decoding.
internal/
├── dht/ # BEP 5 — Kademlia DHT peer discovery
├── protocol/ # Wire protocol, BEP 10 extension, BEP 9 metadata
├── tracker/ # HTTP (BEP 3) + UDP (BEP 15) tracker client
├── peer/ # Background peer discovery manager + inbound listener
├── state/ # Persistent resume state
├── types/ # Core data structures + .torrent/.magnet parser
├── downloader/ # Adaptive resumable download engine + telemetry
├── choke/ # Choking/unchoking round manager
├── portmap/ # UPnP + NAT-PMP abstraction
├── natpmp/ # NAT-PMP implementation
└── upnp/ # UPnP implementationThe BitTorrent Handshake
Every peer connection starts with a 68-byte handshake:
<1 byte: pstrlen=19><19 bytes: "BitTorrent protocol">
<8 bytes: reserved (extension flags)>
<20 bytes: info_hash>
<20 bytes: peer_id>The reserved bytes are what make extensions possible. Bit 20 of byte 6 (0-indexed) signals BEP 10 extension protocol support. Here’s how the client advertises it:
func HandshakeExtended(conn net.Conn, infoHash [20]byte, peerID [20]byte) ([20]byte, bool, error) {
reserved := [8]byte{0, 0, 0, 0, 0, 0x10, 0, 0}
if err := WriteHandshakeWithReserved(conn, infoHash, peerID, reserved); err != nil {
return [20]byte{}, false, err
}
remotePeerID, remoteReserved, err := ReadHandshakeFull(conn, infoHash)
if err != nil {
return [20]byte{}, false, err
}
return remotePeerID, remoteReserved[5]&0x10 != 0, nil
}After the handshake, all communication happens through length-prefixed messages:
<4 bytes: message length><1 byte: message ID><payload>The message IDs are standardised: 0=choke, 1=unchoke, 2=interested, 3=not-interested, 4=have, 5=bitfield, 6=request, 7=piece, 8=cancel, 20=extended.
Peer Discovery: Three Layers
Getting a list of peers to connect to is the first challenge. The client implements three layers, tried in order
Layer 1 & 2 — HTTP and UDP Trackers
HTTP trackers (BEP 3) are simple: build a URL with info hash, peer ID, port, and event, make a GET request, bencode-decode the response.
UDP trackers (BEP 15) are faster and more common. The protocol is:
- Connect: send a magic 64-bit connection ID (
0x41727101980), get back a real connection ID. - Announce: send info hash, peer ID, download stats, and event; get back a compact peer list.
// Connect phase — get a real connection ID from the tracker
connectReq := make([]byte, 16)
binary.BigEndian.PutUint64(connectReq[0:], 0x41727101980) // magic
binary.BigEndian.PutUint32(connectReq[8:], 0) // action: connect
binary.BigEndian.PutUint32(connectReq[12:], txID)
conn.Write(connectReq)
conn.Read(connectResp)
connectionID := binary.BigEndian.Uint64(connectResp[8:])The announce phase then embeds this connection ID, the info hash, peer ID, downloaded/uploaded/left bytes, and the lifecycle event (started, completed, stopped).
Layer 3 — DHT (BEP 5): Kademlia Peer Discovery
DHT is the most interesting layer — it lets the client find peers without any tracker at all. It uses the Kademlia distributed hash table algorithm.
Key concepts:
- Every node has a random 160-bit ID.
- Distance between nodes is defined as XOR of their IDs.
- The network is self-organizing: nodes closer in XOR-space know more about keys in that space.
The lookup algorithm is iterative:
- Start with the 4 public bootstrap nodes (
router.bittorrent.com,router.utorrent.com, etc.). - Send
get_peers(info_hash)to thealpha=3closest unqueried nodes. - Each node returns either a list of peers (if it knows any), or a list of even-closer nodes.
- Repeat with the new closer nodes until no closer nodes are returned or enough peers are found.
// Each round queries alpha=3 nodes in parallel
for _, c := range batch {
wg.Add(1)
go func(c *candidate) {
defer wg.Done()
pp, nn, err := krpcGetPeers(c.addr, localID, infoHash, 8*time.Second)
if err != nil { return }
addPeers(pp)
addNodes(nn) // newly discovered nodes are sorted by XOR distance
}(c)
}All communication uses KRPC — a bencode-encoded RPC protocol over UDP. The candidate list is kept sorted by XOR distance to ensure the lookup converges on the target.
Magnet Links: Fetching Metadata from the Swarm (BEP 9)
A magnet link gives you only the info hash and optionally some tracker URLs — no piece hashes, no file list. The torrent metadata (the info dictionary) has to be fetched from peers.
Get Abhishek Kushwaha’s stories in your inbox
Join Medium for free to get updates from this writer.
This is where BEP 10 + BEP 9 come together:
- BEP 10 extension handshake: After the BitTorrent handshake, exchange an
extendedmessage (ID 20) advertising which extensions you support. The peer replies with its own capability map, including the extension ID it assigned tout_metadata. - BEP 9
ut_metadata: The metadata is split into 16 KiB blocks. Request each block by index, collect all blocks, concatenate, and SHA-1 hash the result — it must equal the info hash from the magnet link.
// Extension handshake payload (bencode)
{
"m": {"ut_metadata": 1},
"metadata_size": 0
}
// Metadata request (after learning peer's extension IDs)
{
"msg_type": 0, // request
"piece": <block_index>
}
// Assemble and verify
sha1.Sum(assembled) == infoHash // must matchThe client probes multiple peers in parallel — the first one to return valid metadata wins.
The Download Engine: Adaptive Peer Scheduling
Once we have the piece hashes, the actual download begins. This is the most complex part of the codebase.
Rarest-First Scheduling
To maximize swarm health, the client downloads the pieces that fewest peers have first. This ensures rare pieces are spread widely rather than becoming bottlenecks.
Pipelined Block Requests
Each piece is split into 16 KiB blocks. Instead of request→wait→receive→request, the client pipelines multiple requests to the same peer:
→ Request block 0
→ Request block 1
→ Request block 2 (all in flight simultaneously)
← Receive block 0
← Receive block 1
→ Request block 3 (refill pipeline)
...This keeps the TCP pipe full and is critical for download speed.
Adaptive Peer Scoring and Quarantine
Not all peers are equal. Peers that time out, send corrupt data, or disconnect frequently get their score reduced and are eventually quarantined — temporarily excluded from new work assignments. This prevents a handful of bad peers from stalling the download.
Endgame Mode
Near the end of a download, a few pieces are held by only a handful of peers, and stalls are common. The client enters endgame mode when only a small number of pieces remain: it sends the same piece request to multiple peers simultaneously. When one delivers the piece, it sends cancel messages to the others. This dramatically reduces tail-completion time.
SHA-1 Integrity Verification
Every downloaded piece is verified against the SHA-1 hash from the .torrent file before being written to disk. A corrupt piece is discarded and re-queued. This makes the client robust against faulty peers and network corruption.
Resumable Downloads and Persistent State
The client saves download progress to a state file alongside the output. On restart, it reads the state file and verifies which pieces are already complete (by re-hashing from disk). Only incomplete pieces are downloaded, allowing interrupted downloads to resume seamlessly.
UPnP and NAT-PMP: Becoming Reachable
Most home users are behind NAT. Without port mapping, they can connect out to peers, but peers can’t connect in to them. The client implements both UPnP and NAT-PMP to automatically ask the home router to forward an external port to the client:
if m, err := portmap.NewMapper(3 * time.Second); err == nil {
if eip, _, err := m.MapPort(uint16(*listenPort)); err == nil {
externalIP = eip // announce this IP to trackers
}
}
defer mapper.UnmapPort(uint16(*listenPort))The external IP is then used in tracker announces so remote peers can initiate connections — improving swarm participation.
Tracker Lifecycle Events
The BitTorrent spec requires clients to announce lifecycle events to trackers:
started— first announce when the download begins- periodic — every ~4 minutes during the download (with live downloaded/uploaded/left counters)
completed— when the download finishes successfullystopped— when the client exits (even on error, viadefer)
This is what allows trackers to maintain accurate peer counts and seed ratios.
Private Torrent Safety
Some torrents include a private=1 flag in the info dictionary. For these torrents, DHT and PEX (peer exchange) must be disabled — the torrent owner wants peer discovery to go through their tracker only. The client checks this flag and enforces the restriction throughout.
Using the Client
# Build
git clone https://github.com/Abhicodeitout/Bit_torrent.git
cd Bit_torrent
go build -o bin/torrent-client ./cmd/torrent-client/
# Download from a .torrent file
./bin/torrent-client ubuntu-24.04.iso.torrent
# Download from a magnet link
./bin/torrent-client "magnet:?xt=urn:btih:AABBCCDD...&tr=udp://tracker.opentrackr.org:1337/announce"
# Flags
./bin/torrent-client --verbose --listen-port 51413 file.torrent
./bin/torrent-client --quiet file.torrent
./bin/torrent-client --enable-nat=false file.torrentFiles are saved to $HOME/Downloads/. Multi-file torrents are assembled into a subdirectory with the full original directory tree.
Lessons Learned
1. The protocol is deceptively layered. BEP 3 is the base, but without BEP 10, BEP 9, and BEP 15 you can barely function in the modern swarm. Plan to implement them together.
2. Peer quality varies enormously. A significant fraction of peers in the swarm are unreachable, silently drop connections, or send garbage. Robust timeout handling, scoring, and quarantine are not optional.
3. Endgame mode is not optional. Without it, the last few pieces of a large download can take longer than all the previous pieces combined.
4. Go is an excellent fit. Goroutines make pipelining and parallel peer probing natural. The standard library covers TCP, UDP, crypto/sha1, encoding/binary, and context — the only external dependency needed was a bencode library.
5. DHT is worth the complexity. The ability to download a torrent with no tracker at all — just a 40-character hex string — is remarkable. The Kademlia paper is dense but the implementation is manageable once you understand XOR distance and iterative lookup.
What’s Next
- Seeding: The upload path for outbound connections is partially implemented. Full seeding support is the next major milestone.
- Web UI: A real-time progress dashboard.
- Streaming: Prioritize pieces in sequential order for media files.
Try It
The full source is on GitHub: github.com/Abhicodeitout/Bit_torrent
Even you can contribute to this development.
It builds with a single go build command and has no system dependencies. Tested on Linux, macOS, and Windows against real public torrents.
If you’re interested in distributed systems, peer-to-peer protocols, or just want to see how Go handles network-heavy concurrent code — clone it, read through it, and download something.
If you found this useful, leave a clap and follow for more systems-programming deep dives.









