# bittorrent-dht gives a Node client trackerless peer discovery

> bittorrent-dht is a JavaScript implementation of the BitTorrent DHT protocol, providing peer discovery without a tracker, BEP44 storage for arbitrary data, and a routing table you can persist between restarts. It suits Node developers building torrent clients, and it depends on an open UDP port and on the public DHT staying reachable.

**webtorrent/bittorrent-dht** — 🕸 Simple, robust, BitTorrent DHT implementation

- Repository: https://github.com/webtorrent/bittorrent-dht
- Website: https://webtorrent.io
- Stars: 1,284 · Forks: 206
- Language: JavaScript
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/webtorrent-bittorrent-dht

## Peer discovery with no tracker in the middle

BitTorrent's original design leaned on a tracker: a server that knew which peers held which torrent and told everyone where to look. The DHT replaced that with a distributed hash table spread across every participant, which is what makes trackerless torrents possible. bittorrent-dht is a Node.js implementation of that protocol, following the BEP 5 specification, and the project states that the DHT is the main peer discovery layer for BitTorrent.

The audience is a developer writing the client side of a torrent application, in JavaScript, who does not want to implement a distributed hash table from scratch. The module is used by WebTorrent, and the feature list also names peerflix and Playback as projects that use it, which tells you the intended scale: a library that runs in a Node process and is expected to sit next to a tracker implementation or a WebRTC transport.

The package is MIT licensed, published to npm as bittorrent-dht, and the current version is 11.0.12, released on 2026-05-25. The repository is not archived and the last push was on 2026-10-15, so the code is being worked on even where the version number has not moved recently. The previous tags, 11.0.11 in September 2025 and 11.0.10 in May 2025, show a steady patch cadence rather than a burst of feature releases.

## A routing table, tokens and the recursive lookup

The mechanism is the standard Kademlia-style table. The module keeps a routing table of DHT nodes, organised in buckets, and the k-bucket package provides the bucket structure while k-rpc implements the RPC layer the protocol is built on. Node identifiers are 160-bit values, generated randomly by default, and the hash function is SHA1 by default with an option to substitute your own.

Lookup is recursive and the project makes a point of the efficiency: the recursive lookup algorithm minimises UDP traffic, and multiple concurrent lookups can share the same routing table. That last property is the one that matters for a real client, since a user who adds ten torrents at once should not pay for ten independent table warm-ups.

Announce is where the protocol's security mechanism lives. To announce yourself as a peer for a torrent you must present a token obtained from a node during a recent query, and the tokens are what stop one host from signing up another for torrents it does not have. The module states that all token management is handled internally, so the caller does not deal with it. There is also a useful performance detail: if announce runs less than five minutes after a lookup, the routing table from that lookup is reused because the tokens are still valid, while an announce with no cached table triggers a lookup first and takes longer.

The tuning knobs are exposed as constructor options. concurrency caps the number of simultaneous UDP requests at 16 by default, timeBucketOutdated sets the bucket expiry check to 15 minutes, and maxAge controls how long announced peers are kept.

## BEP44 storage and the ed25519 signature requirement

Beyond peer discovery the module implements BEP 44, which allows arbitrary data to be stored in the DHT rather than only peer addresses. The DHT then holds both nodes and values, and the state export reflects that with a nodes array and a values object.

Storing arbitrary data is not a free-for-all, and the module makes the requirement explicit. To use the store operation you have to set a verify option to an ed25519 supercop or ref10 implementation, and the verify function is called with the signature, the value and the public key, and must return whether that signature belongs to that key. The documented example wires in the ed25519-supercop package:

```js
import ed from 'ed25519-supercop'
const dht = new DHT({ verify: ed.verify })
```

The reason is the same token logic that guards announce. Without a signature check, anyone could publish values under any key and overwrite other people's data. With it, only the holder of the private key can write under that public key. The practical consequence for an adopter is that the ed25519 dependency is not optional decoration, it is the thing that makes the store safe to use, and the developer needs a separate package to provide it.

## Installing the module and running a first lookup

The module installs from npm:

```bash
npm install bittorrent-dht
```

The example in the project documentation also installs magnet-uri, which is used to parse a magnet link into an info hash:

```bash
npm install magnet-uri
```

The client code creates the instance, starts listening, and reacts to peer events:

```js
import DHT from 'bittorrent-dht'
import magnet from 'magnet-uri'

const uri = 'magnet:?xt=urn:btih:e3811b9539cacff680e418124272177c47477157'
const parsed = magnet(uri)

console.log(parsed.infoHash) // 'e3811b9539cacff680e418124272177c47477157'

const dht = new DHT()

dht.listen(20000, function () {
  console.log('now listening')
})

dht.on('peer', function (peer, infoHash, from) {
  console.log('found potential peer ' + peer.host + ':' + peer.port + ' through ' + from.address + ':' + from.port)
})

// find peers for the given torrent info hash
dht.lookup(parsed.infoHash)
```

The listen method takes a port, an optional address and an optional callback, and if the port is undefined an available one is picked automatically. Peers arrive as events, and the lookup call also accepts a callback that fires when the recursive lookup has terminated, with an error or null and the number of nodes that turned out to have peers. The lookup returns an abort function, so a client can cancel a search that is no longer wanted.

## Persisting the routing table so restarts are cheap

The specification recommends that a client keeps its DHT nodes between restarts, and the module supports that directly. The state export returns the current state of the DHT, including the nodes and the BEP44 values:

```json
{
  "nodes": [],
  "values": {}
}
```

Each node in the array is an object with host and port properties. Restoring is a loop over that array with the addNode method, and the documented pattern is to destroy the first instance and create a second one with the same table:

```js
const dht1 = new DHT()

// some time passes ...

// destroy the dht
const nodes = dht1.toJSON().nodes
dht1.destroy()

// some time passes ...

// initialize a new dht with the same routing table as the first
const dht2 = new DHT()

nodes.forEach(function (node) {
  dht2.addNode(node)
})
```

There is a catch worth reading before you rely on it. addNode only inserts a node if the routing table has room, or if an unresponsive node can be evicted to make space, and otherwise the node is dropped. A table restored from a stale file can therefore be partly discarded, and the client will fill the gaps by bootstrapping again.

The bootstrap defaults are three public routers on port 6881, and the host option, disabled by default, adds the local peer to the table when it is set, which is what you want if your own node should be reachable as a peer.

## Where a tracker-based client fits better

The project points at the sibling module bittorrent-tracker as the companion piece, and the choice between the two approaches is a real design decision rather than a preference.

A tracker is a service you can point at. Discovery is a request to a server that already knows the swarm, the response is a list of peers, and the failure modes are ordinary network failures. The DHT inverts that: every participant stores a slice of the table, discovery is a series of queries across the swarm, and a client that has just started has nothing until it has bootstrapped and built a table. A tracker also gives an operator a place to enforce rules and to know who is participating. The DHT gives you a trackerless swarm, which is what the project means by allowing trackerless torrents.

The practical difference for a Node developer is the amount of state you carry. A tracker client holds a URL and a session. A DHT client holds a routing table, a token cache, an expiry policy for announced peers, and a UDP port that has to be open. The DHT route also means your client contributes to the network it queries, since that reciprocity is the protocol.

The failure mode to design around is network shape. The DHT runs over UDP, and a client behind a restrictive corporate firewall or a restrictive mobile network will not reach the public routers, which means the module will never find a peer. A tracker behind the same firewall at least has a chance of being reachable over HTTP, which is the situation where the tracker approach wins outright.

## Offline tests, live tests and the upgrade surface

The test setup is unusual enough to be worth knowing about. The test script runs the standard linter and then the tape suite over the test directory, and the project describes the suite as comprehensive and fully offline, which is what lets a DHT library be tested at all without a live network. A separate script runs the live tests, so the network-dependent cases are opt-in rather than part of the default run.

The dependencies are small and purposeful. bencode handles the wire encoding, k-bucket and k-rpc provide the table and the RPC layer, lru and last-one-wins manage caches with the policy of keeping the newest entry, record-cache stores announced peers, randombytes and debug do what their names suggest. The package is an ES module with a single import export, and it declares support for Node 12.20.0 and later, which is a fairly old floor for a package at version 11.

Releases are automated. The release configuration extends the webtorrent semantic-release setup, and a Renovate configuration handles dependency updates, so the version history is generated from commit messages rather than typed by hand. The project is MIT licensed, with the licence in the LICENSE file and a CHANGELOG.md in the tree. Because the module is a single entry point with a stable event API, a version bump is usually a dependency refresh rather than a rewrite, and the risk of an upgrade is low compared with a library that changes its public surface.

## Conclusion

bittorrent-dht fits Node developers writing a torrent client that has to find peers without a tracker, and teams willing to operate a UDP port and contribute to the public routing table. It does not fit a client that must work behind a restrictive network, or a use case where a controlled tracker is acceptable. Verify first that the network you deploy in permits outbound UDP, that the bootstrap nodes you pass are reachable from it, and that your announce path reuses a routing table rather than performing a fresh lookup for every torrent.

## FAQ

### How do I install bittorrent-dht and look up peers for a torrent?

Install it with npm install bittorrent-dht, then create a DHT instance, call dht.listen and trigger a search with dht.lookup(infoHash). Peers are reported through the peer event as they are discovered, and the lookup call also accepts a callback that fires when the search terminates.

### What does dht_store need to work in bittorrent-dht?

The store operation needs a verify option pointing at an ed25519 supercop or ref10 implementation, called as opts.verify(signature, value, publicKey) and returning a boolean. The documented example passes the verify function from the ed25519-supercop package.

### Can I keep the DHT routing table between restarts in bittorrent-dht?

Yes. dht.toJSON() returns the current state including the nodes and BEP44 values, and you restore them with addNode on a new instance. A node is only added if the table has space or an unresponsive node can be evicted to make room.

### Why does my bittorrent-dht client find no peers?

The DHT runs over UDP and the client needs an open port plus reachable bootstrap nodes, which default to router.bittorrent.com, router.utorrent.com and dht.transmissionbt.com on port 6881. A restrictive firewall blocks the queries, and the announce path also needs tokens that are only valid for a short window after a lookup.

## Sources

- [License: MIT](https://github.com/webtorrent/bittorrent-dht/blob/master/LICENSE)
- [Project website](https://webtorrent.io)
- [README](https://github.com/webtorrent/bittorrent-dht/blob/master/README.md)
- [Releases](https://github.com/webtorrent/bittorrent-dht/releases)
- [webtorrent/bittorrent-dht on GitHub](https://github.com/webtorrent/bittorrent-dht)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/webtorrent-bittorrent-dht
