Skip to content
{.used.}

Connection Manager Configuration

This tutorial demonstrates how to configure the connection manager through SwitchBuilder as an API showcase. The connection manager controls how many simultaneous connections a switch accepts, and can optionally trim low-scoring peers via watermark logic.

You'll find all configuration options in the withMaxConnections, withMaxInOut, and withWatermarkPolicy builder methods.

import chronos, strformat

import libp2p
Helper used to create SwitchBuilder with basic options shared in all scenarios.
proc createBaseBuilder(): SwitchBuilder =
  return SwitchBuilder
    .new()
    .withRng(newRng())
    .withAddress(MultiAddress.init("/ip4/127.0.0.1/tcp/0").tryGet())
    .withTcpTransport()
    .withMplex()
    .withNoise()

proc connectPeer(switch: Switch, name: string, description: string) {.async.} =
  await switch.start()

  let peer = createBaseBuilder().build()
  await peer.start()
  await peer.connect(switch.peerInfo.peerId, switch.peerInfo.addrs)

  let count = switch.connectedPeers(In).len
  echo fmt"{name:<60} connected = {count}", "\t", description

  await allFutures(switch.stop(), peer.stop())
The seven configurations

Each block below creates a switch with a different connection-manager configuration, starts it, connects one peer, prints the connected-peer count, then shuts everything down cleanly.

proc main() {.async.} =

1. .build() — default shared limit at 50

When no connection-limit option is supplied the switch uses a default, shared semaphore capped at MaxConnections (50). Both incoming and outgoing connections draw from the same pool, so the total can never exceed 50.

  block:
    let switch = createBaseBuilder().build()

    await connectPeer(switch, "1. .build()", "(shared limit: 50)")

2. .withMaxConnections(100) — raise the shared cap

Both incoming and outgoing connections share one semaphore of size 100. The 101st connection attempt raises error on the dialing side (outgoing) or blocks until a slot is released (incoming).

  block:
    let switch = createBaseBuilder().withMaxConnections(100).build()

    await connectPeer(switch, "2. .withMaxConnections(100)", "(shared limit: 100)")

3. .withMaxInOut(30, 20) — independent per-direction caps

Two separate semaphores are created: 30 slots for incoming connections and 20 slots for outgoing dials. Outbound traffic can never exhaust the inbound pool and vice-versa, giving finer control over how bandwidth is allocated.

  block:
    let switch = createBaseBuilder().withMaxInOut(30, 20).build()

    await connectPeer(
      switch, "3. .withMaxInOut(30, 20)", "(in-limit: 30, out-limit: 20)"
    )

4. .withWatermarkPolicy(10, 20) — soft trimming, no hard cap

No semaphore is created — the switch never blocks an incoming connection based on count alone. Instead, once the connected-peer count exceeds the high-water mark (20), the connection manager asynchronously trims down to the low-water mark (10) by closing the lowest-scoring peers. Peers within the grace period (default 1 min) and protected peers are skipped during trimming.

  block:
    let switch = createBaseBuilder().withWatermarkPolicy(10, 20).build()

    await connectPeer(
      switch, "4. .withWatermarkPolicy(10, 20)",
      "(no hard cap; trims to 10 once 20 are connected)",
    )

5. .withWatermarkPolicy(10, 20, gracePeriod = 30.seconds, silencePeriod = 5.seconds) — custom timing

gracePeriod protects newly connected peers from being trimmed: any peer that connected less than gracePeriod ago is skipped when the connection manager prunes down to lowWater. Shortening it from the default 1 minute to 30 seconds makes the trimmer willing to drop peers sooner after they connect.

silencePeriod is a cooldown between successive trim cycles: once a trim run finishes, the next cycle cannot start until silencePeriod has elapsed. Reducing it from the default 10 seconds to 5 seconds lets the connection manager react faster when many peers connect in quick succession.

Tune these two durations together to balance connection churn against responsiveness: a long grace period preserves recently opened connections longer, while a short silence period allows more frequent pruning passes.

  block:
    let switch = createBaseBuilder()
      .withWatermarkPolicy(10, 20, gracePeriod = 30.seconds, silencePeriod = 5.seconds)
      .build()

    await connectPeer(
      switch, "5. .withWatermarkPolicy(gracePeriod=30s, silencePeriod=5s)",
      "(trims at 20; grace 30 s; silence 5 s)",
    )

6. .withWatermarkPolicy(10, 20).withMaxConnections(30) — hard cap + trimming

A semaphore enforces an absolute ceiling of 30 connections while watermark trimming keeps the active peer count near 10 long before that ceiling is ever reached. Both guards operate simultaneously: the semaphore blocks new connections at the limit, and the trimmer prunes existing ones once the high-water mark is hit.

  block:
    let switch =
      createBaseBuilder().withWatermarkPolicy(10, 20).withMaxConnections(30).build()

    await connectPeer(
      switch, "6. .withWatermarkPolicy(10,20).withMaxConnections(30)",
      "(hard cap: 30; trims at 20)",
    )

7. .withWatermarkPolicy(10, 20).withMaxInOut(30, 20) — per-direction caps + trimming

The most granular configuration: two independent semaphores (30 incoming, 20 outgoing) combined with watermark trimming. New connections are blocked by the per-direction caps, and existing connections are pruned asynchronously once the total peer count exceeds 20.

  block:
    let switch =
      createBaseBuilder().withWatermarkPolicy(10, 20).withMaxInOut(30, 20).build()

    await connectPeer(
      switch, "7. .withWatermarkPolicy(10,20).withMaxInOut(30,20)",
      "(in-limit: 30, out-limit: 20; trims at 20)",
    )

waitFor(main())
Running this program with nim c -p:../ -d:chronicles_log_level=error -r tutorial_5_connmanager.nim produces one line per scenario, each showing connected = 1, confirming that each configuration accepts connections normally under its limit.

Summary

Builder call Effect
.build() Shared limit at 50 (default MaxConnections)
.withMaxConnections(100) Shared limit at 100
.withMaxInOut(30, 20) Separate limits: 30 incoming / 20 outgoing
.withWatermarkPolicy(10, 20) No hard cap; trims to 10 once 20 connected
.withWatermarkPolicy(10, 20, gracePeriod = 30.seconds, silencePeriod = 5.seconds) Custom trim timing: 30s grace, 5s cooldown
.withWatermarkPolicy(10, 20).withMaxConnections(30) Hard cap at 30 + trim at 20
.withWatermarkPolicy(10, 20).withMaxInOut(30, 20) Separate limits + trim at 20