libp2p Kademlia DHT Configuration Parameters
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 thekclosest 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
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
So, you want to use a DHT? Let us help you configure it correctly.
From public servers to private clusters: a practical configuration walkthrough for every libp2p DHT deployment pattern.
Optimistic Provide: How We Made IPFS Content Publishing 10x Faster
How a statistical approach cut IPFS content publishing times by over one order of magnitude.
Peering into Privacy: A Deep Dive into the Monero Network Topology
Mapping Monero's P2P network: 16,000 nodes, one dominant provider, and a lot of spy nodes.