libp2p Kademlia DHT Configuration Parameters

Yiannis Psarras Dennis Trautwein

Introduction

This is the parameter reference document for the libp2p Kademlia DHT. It accompanies the configuration guide and documents each configurable parameter in detail across both go-libp2p-kad-dht and rust-libp2p/kad.

The libp2p Kademlia DHT is an S/Kademlia-derived protocol adapted for adversarial public internet environments. Its routing layer organizes peers into a binary tree of k-buckets structured by the XOR distance metric between SHA-256-derived peer identifiers. Three scalar parameters from the original Kademlia paper govern the fundamental performance envelope:

  • k (bucket size) determines how many peers each bucket holds and, by extension, how many nodes store a given record. Higher values improve resilience to churn and partial network partitions at the cost of replication bandwidth. The canonical value across all libp2p implementations is 20.
  • α (concurrency) caps the number of parallel in-flight RPCs during an iterative lookup. It controls the latency-bandwidth trade-off: increasing α accelerates convergence in low-latency networks but amplifies load on heavily queried keyspace regions. The default in Go is 10, in Rust it is 3.
  • β (resiliency, also called the replication factor in some contexts) sets how many of the k closest peers must have responded before a query path is considered complete. It directly affects both lookup termination latency and the probability of finding a live record under churn. The default is 3.

Beyond these core parameters, practical deployments require configuration across four additional functional layers: routing table maintenance, operational mode, record lifecycle, and security filtering.

go-libp2p-kad-dht is the reference implementation, used by Kubo and the majority of production IPFS nodes on the Amino DHT. rust-libp2p/kad is the Rust implementation, used by projects such as Polkadot and various independent libp2p deployments. The two share the same wire protocol and interoperate, but expose different configuration surfaces and carry different defaults.

Parameter Overview

The table summarises every configuration parameter covered in this document. The default column shows the Go value, with the Rust value in parentheses where it differs. Parameter availability per implementation is noted in the description column where relevant. Source code references for each parameter appear in the linked subsections.

Parameter Default Description
Core Kademlia
Bucket Size 20 Peers per k-bucket and closest peers contacted per lookup
Query Parallelism 10 (Rust: 3) Maximum in-flight RPCs per iterative lookup
Query Resiliency 3 (Rust: k) Closest peer responses required for query termination
Query Timeout 10 s (Rust: 60 s) Hard deadline applied to each iterative query
Routing Table
Refresh Period 10 min (Rust: 5 min) Interval between proactive bucket refresh sweeps
Latency Tolerance 10 s Maximum tolerated RTT before peer eviction (Go only)
Auto Refresh enabled Whether background refresh queries are issued at all
Bucket Insertion Strategy OnConnected When a peer is admitted to the routing table (Rust only)
Pending Entry Timeout 60 s Lifetime of a pending bucket candidate (Rust only)
Operational Mode
Operating Mode ModeAuto (Rust: Client) Whether the node accepts inbound DHT queries
Record Management
Value Record TTL 48 h Maximum age of a stored value record
Record Replication Interval 1 h How often stored records re-replicate to neighbours (Rust only)
Record Publication Interval 22 h How often the originator re-publishes value records
Provider Record TTL 48 h Maximum age of a stored provider advertisement
Provider Publication Interval 22 h (Rust: 12 h) How often the advertising node re-announces
Provider and Value Store both enabled Whether record subsystems handle their RPCs (Go only)
Inbound Record Filtering Unfiltered Whether the application validates inbound records (Rust only)
Lookup Caching Enabled(1) Write-back cache target count after a lookup (Rust only)
Security and Peer Admission
IP Diversity Filter 3 per /16 Per-subnet cap on routing table entries (Go only)
Admission Check Concurrency 256 Parallel pre-admission reachability probes (Go only)
Query Peer Filter accept all Predicate gating peers dialled during a query (Go only)
Routing Table Filter accept all Predicate gating routing table admission (Go only)
Disjoint Query Paths false S/Kademlia non-overlapping path lookups (Rust only)
Protocol Identity
Protocol Identifier /ipfs DHT overlay namespace separator

Core Kademlia Parameters

Bucket Size

Go: BucketSize · Rust: set_kbucket_size (K_VALUE)

Default: Go 20 (Amino-locked) | Rust 20

The maximum number of peers stored per k-bucket, and the number of closest peers contacted per lookup. Changing k changes the replication depth of every lookup and provide operation across the network.

Higher: More replication copies in the DHT, better record survival under churn, harder eclipse attacks. Bandwidth and memory scale linearly: each provide operation contacts k peers.

Lower: Less per-operation bandwidth and routing table memory. k = 10 is workable on stable, low-churn private networks with a small and trusted peer set.

When to change: Do not change on any network using the /ipfs protocol prefix; the Go config validator rejects it. For a private DHT fork, or a stable network, k = 10-15 is reasonable. Keep k ≥ 20 for adversarial environments.

Query Parallelism

Go: Concurrency (DefaultConcurrency) · Rust: set_parallelism (ALPHA_VALUE)

Default: Go 10 | Rust 3

The number of simultaneous in-flight RPCs during an iterative lookup. This is the primary lever for controlling lookup latency. The Go default of 10 was tuned empirically for the Amino DHT; Rust retains the Kademlia paper’s α = 3.

Higher: Compresses lookup latency by fanning out to more peers in parallel. Returns diminish beyond α ≈ 10 as the bottleneck shifts from the number of in-flight requests to the slowest responding peer.

Lower: Reduces connection churn and resource consumption. α = 1 produces a sequential walk: correct but slow. Values of 3–5 suit constrained clients.

When to change: Reduce to 3–5 for IoT, mobile, or browser nodes. Rust’s default of 3 is conservative; increasing to 6–10 can meaningfully reduce lookup latency on well-connected server nodes.

Query Resiliency

Go: Resiliency · Rust: set_replication_factor

Default: Go β = 3 | Rust replication factor = 20

In Go, β sets the number of closest peers that must have responded before a lookup is declared complete. In Rust, set_replication_factor covers both the lookup fan-out and the termination criterion (it bundles what Go separates into k and β).

Higher: Queries collect more responses before terminating, improving the chance of finding the freshest record under churn. Latency increases because the query waits for more peers.

Lower: Queries terminate on the first satisfactory response set. Fastest possible termination, but the result may be stale under high churn.

When to change: β = 3 is sensible for public-network record retrieval. Increase to 5–10 for applications requiring high confidence in record freshness (e.g. IPNS, mutable state). Reduce to 1 for high-throughput batch operations where an approximate result is acceptable.

Query Timeout

Go: RoutingTableRefreshQueryTimeout · Rust: set_query_timeout

Default: Go 10 s (refresh queries only) | Rust 60 s (all iterative queries)

The hard deadline applied to each iterative query. In Go this applies specifically to routing table refresh queries; in Rust it covers all iterative queries.

Higher: Accommodates high-latency paths and temporarily overloaded peers.

Lower: Prunes non-responsive peers faster and keeps refresh cycles tight. Too short on global networks risks abandoning reachable peers with variable RTT.

When to change: The Go default of 10 s is appropriate for well-connected nodes (intercontinental RTT is typically 100–300 ms, so 10s is generous). Increase to 20–30 s for satellite or high-latency links. Reduce to <5 s for LAN-only deployments.

Routing Table Management

A well-maintained routing table is the single largest driver of consistent lookup performance: a stale table forces expensive full-network walks to compensate for missing close peers. Parameters in this group control how the routing table is populated and how freshness is maintained over time.

Refresh Period

Go: RoutingTableRefreshPeriod · Rust: set_periodic_bootstrap_interval

Default: Go 10 min | Rust 5 min

How often the node proactively queries a random target in each under-populated bucket to discover new peers.

Higher (less frequent): Less background connection load at the cost of routing table staleness. The table drifts from the current network topology; lookups compensate by doing more iterative rounds.

Lower (more frequent): Tighter routing table and lower per-query latency, but continuous background traffic.

Disable: Only appropriate when the application drives refresh explicitly or in test harnesses. Never disable on a production server node. In Go: DisableAutoRefresh. In Rust: set_periodic_bootstrap_interval(None).

When to change: Extend to 30–60 min for resource-constrained clients that query infrequently. The routing table rebuilds on demand from the first lookup. Reduce toward 3–5 min for indexers or pinning services doing continuous high-frequency lookups.

Latency Tolerance

Go: RoutingTableLatencyTolerance · Rust:

Default: Go 10 s | Go only

The maximum RTT a peer may have before it becomes eligible for eviction in favour of a lower-latency alternative.

Higher (more tolerant): Retains geographically diverse peers even with variable RTT. Useful when routing through high-latency but keyspace-relevant peers is worth the cost.

Lower (stricter): Produces a routing table biased toward low-latency peers, generally improving lookup speed. The risk is premature eviction of peers with variable but acceptable RTT.

When to change: Leave at 10 s for public server nodes. Tighten to 3–5 s when lookup latency is the primary objective and the deployment is geographically concentrated.

Auto Refresh

Go: DisableAutoRefresh · Rust: set_periodic_bootstrap_interval(None)

Default: enabled in both

When disabled, the node stops issuing background refresh queries entirely. The routing table is only populated from incoming and outgoing connections.

Disable: Appropriate only when the application manages routing table refresh explicitly (for example, a controlled crawler that triggers refresh on a custom schedule) or in test harnesses. A production server node with auto refresh disabled will have a deteriorating routing table under churn and deliver increasingly poor lookup quality.

When to change: Almost never. If background refresh traffic is a concern, prefer increasing the Refresh Period rather than disabling it entirely.

Bucket Insertion Strategy

Go: — · Rust: set_kbucket_inserts

Default: Rust OnConnected | Rust only

Controls when peers are admitted into the routing table. OnConnected adds peers on every established connection. Manual requires the application to call Behaviour::add_address explicitly.

OnConnected: Fills the routing table opportunistically from all connections. May admit transiently connected peers, adding churn.

Manual: Application controls routing table membership exactly. Appropriate for permissioned or consortium networks where routing table admission is a policy decision.

When to change: Use Manual for networks where routing table membership is controlled (allow-lists, authenticated peers, private overlays). Keep OnConnected for public DHT participation.

Pending Entry Timeout

Go: — · Rust: set_kbucket_pending_timeout

Default: Rust 60 s | Rust only

When a peer cannot immediately enter a full bucket, it occupies a pending slot. After this timeout without an eviction opportunity, the pending entry is dropped.

Higher: Gives new peers more time to displace unresponsive incumbents. Useful when the network has high connection variability.

Lower: Drops pending candidates faster, limiting the memory footprint of the pending queue.

When to change: The default is appropriate for most deployments. Increase if the network has high connection variability and peers frequently fail to displace stale bucket entries.

Operational Mode

Mode determines whether a node is visible to others as a DHT participant or operates purely as a client. This is a correctness parameter rather than a performance parameter. Running a node in the wrong mode actively degrades routing for other participants.

Operating Mode

Go: Mode · Rust: set_mode

Default: Go ModeAuto | Rust Client (must be set explicitly to server)

Mode Accepts inbound queries Visible to others When to use
Client No No NAT-restricted nodes, browsers, mobile
Server Yes Yes Publicly reachable infrastructure
ModeAuto (Go) Depends on reachability When reachable Default; handles dynamic NAT status
ModeAutoServer (Go) Yes until proven unreachable Always until proven otherwise Controlled infra where reachability is expected but slow to confirm

Running in server mode from behind NAT inserts phantom entries into other nodes’ routing tables, poisoning routing for the whole network. Never set server mode unless the node is confirmed publicly reachable.

When to change from ModeAuto: Use ModeAutoServer when operating in controlled infrastructure where public reachability is expected but the reachability event may be slow to fire at startup. Use client mode for any node where inbound connections are structurally impossible.

Record Management

This group controls how long records persist and how frequently they are refreshed. The key invariants are: publication interval must be significantly shorter than TTL (rule of thumb: 2×), and replication interval (Rust) must be shorter than publication interval. Violations cause records to expire before they are refreshed.

Value Record TTL

Go: MaxRecordAge (DefaultProvideValidity) · Rust: set_record_ttl

Default: Go 48 h | Rust 48 h

Applies only to deployments that store value records.

Maximum time a node stores a received value record. Records exceeding this age are deleted regardless of their embedded validity claims.

Higher: Reduces re-publication pressure. Setting to None (Rust) means records never expire and is only safe in closed, controlled deployments.

Lower: Limits the window for stale or malicious records to persist. Publishers must re-publish more frequently to keep records alive.

When to change: 48 h matches the Amino DHT specification. Reduce for DHTs carrying frequently-updated state. Never use None on a public network. Always ensure publication interval is well below TTL.

Record Replication Interval

Go: — · Rust: set_replication_interval

Default: Rust 1 h | Rust only

Applies only to deployments that store value records.

How often stored records (received from other nodes) are re-replicated to the current closest peers. Replication does not extend a record’s TTL; it only ensures records survive topology changes by copying to newly-responsible nodes.

Higher: Less background traffic, but records may become unavailable if the responsible peer set shifts significantly between publication cycles.

Lower: Tighter consistency under high churn, at the cost of more replication traffic.

Disable (None): Only for read-only record stores or when the application manages replication externally.

When to change: Keep the 1 h default for any deployment with meaningful peer turnover. 4–6 h is sufficient for private low-churn networks.

Record Publication Interval

Go: external (22 h) · Rust: set_publication_interval

Default: Go 22 h (managed outside the DHT) | Rust 22 h

Applies only to deployments that store value records.

How often the originating node re-publishes its value records. Re-publication resets the record’s TTL across all k responsible peers; replication (above) only copies the existing record to peers that became responsible due to topology changes.

Higher: Less bandwidth used for re-publication, at the risk of records expiring if a publisher is offline for an extended period.

Lower: More aggressive re-publication keeps records alive under heavy churn.

When to change: The 22 h default with 48 h TTL gives a 2× safety margin. For highly dynamic networks, reduce to 12 h. For stable networks with guaranteed uptime, extending to 36–40 h is safe.

Provider Record TTL

Go: MaxRecordAge (DefaultProvideValidity) · Rust: set_provider_record_ttl

Default: Go 48 h | Rust 48 h

Applies only to deployments that handle provider records.

Maximum time a node stores a received provider advertisement. Same mechanics as Value Record TTL, applied to provider records specifically.

Higher / None: Records persist longer without re-announcement. None means provider records never expire and is safe only in controlled closed networks.

Lower: Tighter expiry window. Publishers must re-announce more frequently to keep content discoverable.

When to change: 48 h is the Amino DHT specification. Reduce to 12–24 h for time-sensitive provider records. Never use None on a public network.

Provider Publication Interval

Go: external (DefaultReprovideInterval) · Rust: set_provider_publication_interval

Default: Go 22 h | Rust 12 h

Applies only to deployments that handle provider records.

How often the advertising node re-announces its provider records to the DHT. The Rust default of 12 h is almost twice as frequent as Go’s 22 h. Rust nodes generate roughly double the provider-announcement traffic on a shared DHT. This is not an interoperability issue but matters for capacity planning.

Higher: Less bandwidth consumed by re-announcements. Risk of records expiring during outages.

Lower: Higher availability assurance. At scale (millions of CIDs), frequent re-announcement becomes a significant bandwidth cost. See the Reprovide Sweep optimisation.

When to change: 48 h TTL with 22 h reprovide is correct for most deployments. Align Go and Rust defaults to the same interval if both participate in a shared overlay.

Provider and Value Store

Go: DisableProviders · DisableValues · Rust:

Default: both enabled (Amino-locked) | Go only

DisableProviders removes all ADD_PROVIDER / GET_PROVIDERS handling; DisableValues removes PUT_VALUE / GET_VALUE. Both are rejected by the Go config validator when using the /ipfs protocol prefix.

When to disable: Only for custom DHT overlays under a private protocol prefix. Examples include a peer-discovery-only DHT with no content routing, or a DHT where records are handled at the application layer.

Risk of misuse: Disabling either on a public node silently breaks record storage for every peer that contacts it. Do not touch these settings unless building a private DHT fork.

Inbound Record Filtering

Go: — · Rust: set_record_filtering

Default: Rust Unfiltered | Rust only

Not applicable to peer-discovery-only deployments.

Unfiltered forwards incoming records directly to the record store. FilterBoth emits an event instead, allowing the application to validate and explicitly accept or reject the record before storage.

Unfiltered: Zero overhead. No application involvement in record admission.

FilterBoth: Application validates records before storage. Required for custom signature verification, key constraints, or spam protection beyond the default validator.

When to change: Switch to FilterBoth when the application needs custom validation on inbound records (permissioned networks, domain-specific schemas, or tighter spam control).

Lookup Caching

Go: — · Rust: set_caching

Default: Rust Enabled { max_peers: 1 } | Rust only

Applies only to deployments that store value records.

On a successful record lookup, the behaviour tracks up to max_peers closest peers that did not return the record. The application can then write the record back to those peers, implementing standard Kademlia write-back caching.

Higher max_peers: Records migrate toward popular keyspace sectors, reducing future lookup latency for hot keys. Wider distribution can cause consistency issues for mutable records.

Disabled: No caching. Records stay only at their authoritative storage locations. Reduces consistency risk for mutable records like IPNS entries.

When to change: Disable when record consistency must be tightly controlled. Increase max_peers for content-distribution use cases where proximity-weighted caching is desirable.

Security and Peer Admission

This group controls which peers can enter the routing table and which can be contacted during queries. These parameters directly affect Sybil and eclipse attack resistance. Loosening them on public networks carries real security cost. Tightening them on private networks wastes routing table slots.

IP Diversity Filter

Go: RoutingTablePeerDiversityFilter (DefaultMaxPeersPerIPGroup) · Rust:

Default: Go: 3 peers per /16 globally, 2 per /16 per bucket | Go only

Limits how many peers sharing a /16 IPv4 subnet prefix can occupy the routing table. The two Amino defaults cap both the global per-group count and the per-bucket count independently.

Stricter (lower limits): Stronger Sybil resistance. An adversary controlling a /16 block occupies fewer routing table slots. At the extreme (1 per /16 globally), the routing table becomes well-diversified but hard to fill in geographically concentrated networks.

Permissive / disabled: No subnet-based restriction. Appropriate for private datacenter deployments where many honest nodes legitimately share a /16 block, or single-operator networks where Sybil resistance is not a threat.

When to change: Leave at default for public IPFS participation. Override or remove for private datacenter deployments where the /16 restriction prevents legitimate peers from being added.

Admission Check Concurrency

Go: LookupCheckConcurrency · Rust:

Default: Go 256 | Go only

The maximum number of goroutines performing pre-admission lookup checks (lightweight reachability probes run before a new peer is inserted into the routing table).

Higher: Routing table fills faster under burst conditions (e.g. startup). Memory and goroutine overhead grows proportionally.

Lower: Throttles admission rate, limiting peak memory. Slows routing table convergence at startup but has no steady-state impact once the table is full.

When to change: 256 is appropriate for well-resourced server nodes. Reduce to 16–32 for constrained devices.

Query Peer Filter

Go: QueryFilter · Rust:

Default: Go accept all | Go only

A predicate applied to every peer the query engine considers dialling during a lookup. If the predicate returns false, the peer is skipped.

Non-default filter: Restricts lookups to peers that satisfy an application-specific criterion (allow-lists, required protocol extensions, external reputation signals).

When to change: Leave at default for public DHT participation. Use this hook for permissioned networks or privacy-sensitive deployments. It is the correct extension point. Do not replicate the logic elsewhere in the stack.

Routing Table Filter

Go: RoutingTableFilter · Rust:

Default: Go accept all | Go only

A predicate applied when considering whether to add a peer to the routing table. Called after a connection already exists, so the peer is reachable. This filter allows the application to enforce membership policy on top of reachability.

Non-default filter: Enforces policy. Examples include admitting only peers that have authenticated via an application-layer handshake, or that appear on an explicit allow-list.

When to change: Leave at default for public DHT participation. Pair with Query Peer Filter for consistent admission policy across both the routing table and query execution.

Disjoint Query Paths

Go: — · Rust: disjoint_query_paths

Default: Rust false | Rust only

Implements the S/Kademlia extension: iterative lookups travel α independent, non-overlapping paths through the keyspace instead of a single converging search. An adversary infiltrating a contiguous keyspace region cannot simultaneously compromise all paths.

Enable: Stronger resistance to active routing manipulation. Requires more total RPCs (roughly α × k peers contacted) and adds 1–2 extra RTT per path.

Disable: Standard Kademlia behaviour. Lower latency and bandwidth.

When to change: Enable for consortium or permissioned networks where some participants may be Byzantine, or any deployment where an active routing-manipulation adversary is a realistic threat. Leave disabled for public DHT participation, since the IP diversity filter and the protected-buckets mechanism in go-libp2p already provide adequate eclipse resistance at lower cost.

Protocol Identity

Protocol Identifier

Go: ProtocolPrefix · V1ProtocolOverride · Rust: Config::new

Default: Go /ipfs | Rust /ipfs/kad/1.0.0

The protocol identifier partitions the DHT into isolated overlays. Nodes using different prefixes do not exchange routing information or records, even if they share the same transport layer. LAN-local variants use /ipfs/lan/kad/1.0.0 via ProtocolExtension("/lan") in Go. This is how Kubo implements its dual WAN + LAN DHT. In Rust the full protocol string is passed directly to kad::Config::new(StreamProtocol); Config::default() constructs with /ipfs/kad/1.0.0. There is no public setter to advertise multiple protocol names on a single behaviour.

When to change: Any private or application-specific DHT must use a unique prefix. Never use /ipfs for a private DHT. Doing so would cause your nodes to join and pollute the public Amino DHT. The prefix has no performance impact. It is a pure namespace separator.

Related Posts