Repository navigation
Give the content kubo headroom for leaked inbound connection slots - #10
Merged
Merged
Conversation
Every content node had all 4096 inbound connection slots of kubo's resource manager (System.ConnsInbound) in use since its start on 2026-10-14, and refused new peers: 2.4M to 3.1M refused connections per box. The connection manager was not at fault. It counted 277 to 359 connections per box, under HighWater 600, so it had nothing to trim. The slots were leaked reservations, not connections. On 2026-10-19 each box had about 150 live inbound connections (swarm opened minus closed, and the same number of TCP sockets on :4101 in the kernel), against 4096 reserved inbound slots and about 4100 reserved FD slots, with nothing in the handshake (transient) scope. About 3,950 inbound slots, each also holding an FD slot, belong to no connection, so the leak is on the TCP upgrade path of kubo 0.43.1 / go-libp2p v0.49. The bootstrappers leak the same way: 5,400 to 9,000 excess inbound slots each since their restart on 2026-10-16. 4096 is kubo's own choice: it sets System.ConnsInbound to the largest of MaxMemory in MiB (4GiB = 4096), 2 x ConnMgr.HighWater (1200) and a floor, which is why it is not 2 x HighWater here as it was for the bootstrappers. The leak cannot be stopped from the config, and a restart clears it. So: - set System.ConnsInbound to 65536 in libp2p-resource-limit-overrides.json, the file in the repo that replaced Swarm.ResourceMgr.Limits; the init container writes it on every start, from the kubo-config Secret - raise MaxFileDescriptors and IPFS_FD_MAX to 131072, because each leaked slot also holds an FD slot - keep ConnMgr at 600/200, which fits the real load - after a deploy, fail unless the running node reports the 65536 limit A leaked slot holds no socket and no memory, so the higher limit is headroom until the next restart, not extra load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
Since their start on 2026-10-14, every content kubo (sing-1, lith-1, chic-1) has had all 4096 inbound connection slots of the resource manager in use, and has refused new peers: 2.4M to 3.1M refused connections per box (
libp2p_rcmgr_blocked_resources{dir="inbound",resource="connection",scope="system"}).The connection manager is fine. It counts 277 to 359 connections per box, under HighWater 600, so it has nothing to trim.
The slots are leaked reservations, not connections. Read-only snapshot, 2026-10-19:
ss)About 3,950 inbound slots per box belong to no connection, and each holds an FD slot too. Only TCP (and websocket) connections reserve FDs, so the leak is on go-libp2p's TCP upgrade path, in the kubo 0.43.1 build (go-libp2p v0.49). It sits after the transient scope. In practice the node takes only as many inbound peers as it has unleaked slots, about 150, and refuses the rest.
The bootstrappers leak the same way (same kubo). Since their restart on 2026-10-16 about 21:53 UTC, each has 5,400 to 9,000 more inbound slots in use than live inbound connections: 24,619 against 15,632 on sing-1. Up to 2048 of that can be handshaking connections. That is roughly 2,000–3,200 leaked slots a day, so the 64000 limit from #7 runs out around the end of October unless they are restarted first. Not changed here; see the follow-ups.
Why the limit is 4096. kubo sets
System.ConnsInboundto the largest of MaxMemory in MiB (4GiB = 4096), 2 ×ConnMgr.HighWater(1200) and a floor of 800 (core/node/libp2p/rcmgr_defaults.go). For the bootstrappers 2 × HighWater was the largest; for content it is MaxMemory.The change
The leak can't be stopped from the config, and a restart clears it, so this gives the leak room:
roles/content/files/libp2p-resource-limit-overrides.json:System.ConnsInbound65536. This file in the repo is kubo's replacement forSwarm.ResourceMgr.Limits, which kubo no longer accepts. It goes into thekubo-configSecret, and the init container writes it into the repo on every start.Swarm.ResourceMgr.MaxFileDescriptorsandIPFS_FD_MAX: 65536 → 131072, because each leaked slot also holds an FD slot. This is the same value the bootstrappers use.Swarm.ConnMgrstays at 600/200, which fits the real load.ipfs swarm resourcesreports the 65536 limit.A leaked slot holds no socket and no memory, so a higher limit adds no load. Real connections stay capped by ConnMgr, and memory by the 4GiB resource-manager limit and the pod's 8Gi limit. These boxes have 32 cores and 123 GiB, shared with needle and the bootstrappers, and the content pod's limits don't change.
How long 65536 lasts depends on the leak rate once the node stops refusing peers, which can't be measured until it runs unsaturated. Content takes far fewer connections than the bootstrappers, so this should last weeks. The leak metric under Verify shows the real rate.
Rollout (not done; it restarts the content pod)
From the repo root, one box at a time:
Then verify (below), then lith-1, then chic-1. The
--check --diffshould show only thekubo-configSecret (its contents areno_log) and the Deployment's init script andIPFS_FD_MAX.Verify, on each box after its rollout
libp2p_rcmgr_limit{resource="conns_inbound",scope="system"}is 65536, andsystem FDis 131072.libp2p_rcmgr_connections{dir="inbound",scope="system"}is far below 65536.libp2p_rcmgr_blocked_resources{…resource="connection",scope="system"}starts again from zero after the restart and stays there; it no longer rises.libp2p_rcmgr_connections{dir="inbound",scope="system"}minus live inbound connections (sum(libp2p_swarm_connections_opened_total{dir="inbound"}) - sum(libp2p_swarm_connections_closed_total{dir="inbound"})). This gap grows by the number of leaked slots; check it after a day.Follow-ups (not in this PR)
ContentRefusingConnectionsandContentInboundConnectionsAtLimitinkubernetes/manifests/eu/lith1/monitoring/rules/content.yaml(ipni/infra#178) from warning to critical, asrules/bootstrap.yamldoes for the bootstrappers. Also add a warning on the leak gap above (content and bootstrappers) at, say, half the limit, so a restart can be planned before it pages.p2p/net/upgrader/listener.goAccept()drops an upgraded connection that is already closed without callingClose(), so its scope is never released.🤖 Generated with Claude Code