Problem
After boot, peer count quickly decays from MinPeers=50 (and often higher) down to ~10-20 within a few minutes. Observed on live mainnet daemon (f03678816) on commit 525e75a:
boot: 50 peers (DHT seeded rt_size=29, connected=50+)
+~3 min: 33 peers
+~3 min 12s: 30 peers
+~3 min 24s: 20 peers
+~3 min 36s: 19 peers
+~3 min 48s: 14 peers
That's a drop from 50 \u2192 14 in less than a minute of sampling. Way below the configured MinPeers=50 floor.
Why this happens
Two contributing causes:
-
The DHT refresh loop only fires every 5 minutes. dhtRefreshLoop in net/libp2p/dht.go:242 uses opts.RefreshInterval = 5 * time.Minute. So by the time the loop notices peers have dropped, we've been below the target for several minutes. Peer count can decay much faster than that.
-
The refresh loop's trigger is TargetPeers=30, not MinPeers=50. The check is:
if n < opts.TargetPeers { /* re-seed */ }
With TargetPeers defaulting to 30 and MinPeers configured at 50, there's a 20-peer band where we're below the floor but the refresh loop does nothing.
-
The connmgr's low watermark is supposed to handle this but only does so when there are 'reserve' peers known to the peerstore. After bootstrap dials settle, the peerstore mostly empties as connections close, so the connmgr has nothing to dial.
Proposed fix
Add a tight redial keepalive loop alongside the existing 5-minute DHT refresh:
keepaliveInterval := 30 * time.Second
go h.keepaliveLoop(ctx, opts)
Where keepaliveLoop:
- Every 30s, check
h.PeerCount().
- If below
MinPeers (NOT TargetPeers):
- Re-dial all bootstrap peers (cheap, capped at 7 dials)
- Walk the DHT routing table and dial up to N=10 unconnected peers
- Push everything successfully connected back into the DHT routing table via
TryAddPeer
- If at or above
MinPeers: do nothing (don't churn connections)
The existing dhtRefreshLoop stays for the deeper periodic re-bootstrap (5min cadence). The new keepalive is a tighter, cheaper top-up.
Acceptance test
After deploy on the mainnet daemon, peer count should stabilize at or above MinPeers=50 within 30-60s of boot and stay there. Sample 10 times at 30s intervals over 5 minutes:
for i in {1..10}; do
curl -sS http://127.0.0.1:9092/api/dashboard/overview | jq .peers
sleep 30
done
Expected: every sample \u2265 50. Variance \u00b1 5 is fine (connmgr trim during epoch transitions).
Constraints
- Don't be too aggressive: 30s redial is fine; 5s would churn connections needlessly and trigger libp2p backoff.
- Don't bypass the connmgr's grace period (default 20s). Peers we just dialed shouldn't immediately be re-trimmed.
- Stay CGo-free. No new external dependencies.
Files
net/libp2p/dht.go (add keepaliveLoop)
net/libp2p/host.go (kick off the loop from EnableDHT)
Related
- Observed during the v1.2.1 mainnet deploy on 2026-05-22.
- The 5-minute refresh loop landed in commit
665a136 (DHT protocol prefix fix); the 30s keepalive is the follow-up that makes it actually maintain the floor in practice.
Problem
After boot, peer count quickly decays from
MinPeers=50(and often higher) down to ~10-20 within a few minutes. Observed on live mainnet daemon (f03678816) on commit525e75a:That's a drop from 50 \u2192 14 in less than a minute of sampling. Way below the configured
MinPeers=50floor.Why this happens
Two contributing causes:
The DHT refresh loop only fires every 5 minutes.
dhtRefreshLoopinnet/libp2p/dht.go:242usesopts.RefreshInterval = 5 * time.Minute. So by the time the loop notices peers have dropped, we've been below the target for several minutes. Peer count can decay much faster than that.The refresh loop's trigger is
TargetPeers=30, notMinPeers=50. The check is:With
TargetPeersdefaulting to 30 andMinPeersconfigured at 50, there's a 20-peer band where we're below the floor but the refresh loop does nothing.The connmgr's low watermark is supposed to handle this but only does so when there are 'reserve' peers known to the peerstore. After bootstrap dials settle, the peerstore mostly empties as connections close, so the connmgr has nothing to dial.
Proposed fix
Add a tight redial keepalive loop alongside the existing 5-minute DHT refresh:
Where
keepaliveLoop:h.PeerCount().MinPeers(NOTTargetPeers):TryAddPeerMinPeers: do nothing (don't churn connections)The existing
dhtRefreshLoopstays for the deeper periodic re-bootstrap (5min cadence). The new keepalive is a tighter, cheaper top-up.Acceptance test
After deploy on the mainnet daemon, peer count should stabilize at or above
MinPeers=50within 30-60s of boot and stay there. Sample 10 times at 30s intervals over 5 minutes:Expected: every sample \u2265 50. Variance \u00b1 5 is fine (connmgr trim during epoch transitions).
Constraints
Files
net/libp2p/dht.go(addkeepaliveLoop)net/libp2p/host.go(kick off the loop fromEnableDHT)Related
665a136(DHT protocol prefix fix); the 30s keepalive is the follow-up that makes it actually maintain the floor in practice.