Skip to content

fix(stream): remove per-connection sender when connection closes - #6638

Open
Firaenix wants to merge 3 commits into
libp2p:masterfrom
Firaenix:fix/stream-senders-leak
Open

Firaenix wants to merge 3 commits into
libp2p:masterfrom
Firaenix:fix/stream-senders-leak

Conversation

@Firaenix

Copy link
Copy Markdown

Description

Shared::on_connection_closed removes the connection from self.connections but never removes the matching entry from self.senders. Every connection that is ever established leaks one mpsc::Sender<(PeerId, Stream)> entry, keyed by ConnectionId, for the lifetime of the Behaviour.

The leak is functionally invisible - sender() only consults the connections map, so a stale senders entry is never read - which is why it survived this long. It is purely unbounded memory growth proportional to total connections ever accepted, not concurrent connections.

This patch adds the missing self.senders.remove(&conn); alongside the existing self.connections.remove(&conn);.

Production observation

We run a hub on libp2p-stream serving 3k-7k residential peers with high churn (mobile/residential NAT, short-lived connections). Each leaked entry costs roughly 480 bytes (ConnectionId key + mpsc::Sender + hashmap slot overhead), which showed up as a steady 10-22 MB/h RSS growth per instance that scaled with connection churn rate rather than concurrent connection count - the signature of a per-connection-close leak rather than a per-live-connection cost. RSS flattens with this patch applied.

Change

Two files:

  • protocols/stream/src/shared.rs - prune senders in on_connection_closed, plus a test module covering it.
  • protocols/stream/CHANGELOG.md - entry under 0.5.0-alpha.

Tests

Two unit tests added:

  • connection_close_prunes_senders - asserts senders is empty after on_connection_closed for a connection that had a registered sender.
  • reconnect_churn_does_not_accumulate_senders - drives repeated connect/close cycles and asserts senders does not grow across them (the regression that actually bit us).

Notes

The branch is based on a slightly older master and is behind by unrelated commits; the diff is a 2-file patch that applies cleanly to current master. Happy to rebase or reword the changelog entry as preferred.

Firaenix added 3 commits June 11, 2026 11:51
Shared::receiver() inserts an mpsc::Sender into the senders map for
every established connection (inbound and outbound), but
on_connection_closed only cleaned the connections map. ConnectionIds
are unique, so every closed connection permanently leaked one entry —
unbounded memory growth proportional to connection churn.

Observed in production on a relay/dispatch hub serving ~3k-7k
residential peers: ~480 bytes retained per connection close at 6-14
closes/sec, ~10-22 MB/h RSS growth per instance that never plateaus.

The stale entries are functionally invisible (Shared::sender() only
selects candidates via the connections map), so this is a pure leak.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant