Skip to content

Peer count decays after boot; need 30s keepalive redial loop #9

Description

@Reiers

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:

  1. 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.

  2. 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.

  3. 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:

  1. Every 30s, check h.PeerCount().
  2. 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
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingp1High priorityperformancePerformance / latency / throughput

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions