Sequencer Traffic Management

Note

Currently traffic management is supported only on Byzantine Fault Tolerant (BFT) Synchronizers.

This page describes how to enable and configure traffic management on a Synchronizer, and how to inspect and manage the traffic balances of its members.

Inspecting how much traffic a Synchronizer member has from the member’s perspective (for Participant Nodes and Mediator Nodes) is discussed in Manage Node traffic.

Enable traffic management on a Synchronizer

Traffic management can be enabled or disabled on an existing Synchronizer via adjusting its dynamic synchronizer parameters. Please refer to the Change dynamic synchronizer parameters for more details.

First let’s check the current traffic management status of a Synchronizer:

@ sequencer1.topology.synchronizer_parameters.get_dynamic_synchronizer_parameters(sequencer1.synchronizer_id)
res1: DynamicSynchronizerParameters = DynamicSynchronizerParameters(
  confirmation response timeout = 30s,
  mediator reaction timeout = 30s,
  assignment exclusivity timeout = 1m,
  topology change delay = 0.25s,
  ledger time record time tolerance = 1m,
  mediator deduplication timeout = 48h,
  reconciliation interval = 1m,
  confirmation requests max rate = 1000000,
  max request size = 10485760,
  sequencer aggregate submission timeout = 6m,
  ACS commitment catchup = AcsCommitmentsCatchUpParameters(
    catch up interval skip = 5,
    number of intervals to trigger catch up = 2
  ),
  participant synchronizer limits = ParticipantSynchronizerLimits(
    confirmation requests max rate = 1000000
  ),
  preparation time record time tolerance = 24h,
  onboarding restriction = UnrestrictedOpen
)

If the field trafficControl of type TrafficControlParameters is not set (absent in the output above) or has enforceRateLimiting set to false then the traffic management is inactive.

To enable traffic management, you update the dynamic synchronizer parameters, setting the TrafficControlParameters with the enforceRateLimiting = true and specifying desired values for the other parameters documented in configuration class Scaladoc reference.

Note

If you are using a Synchronizer with multiple owners, you need to ensure that the command to enable traffic management is submitted by at least the configured threshold of owners.

Assuming sequencer1 being the only synchronizer owner, run the following command to enable traffic management:

@ import com.digitalasset.canton.config.RequireTypes.{NonNegativeNumeric, PositiveNumeric}
  import com.digitalasset.canton.config.PositiveFiniteDuration
  import com.digitalasset.canton.admin.api.client.data.TrafficControlParameters
  val trafficControlParameters = TrafficControlParameters(
    enforceRateLimiting = true,
    maxBaseTrafficAmount = NonNegativeNumeric.tryCreate(20000L),
    readVsWriteScalingFactor = PositiveNumeric.tryCreate(200),
    maxBaseTrafficAccumulationDuration = PositiveFiniteDuration.ofSeconds(10L),
    setBalanceRequestSubmissionWindowSize = PositiveFiniteDuration.ofMinutes(5L),
    baseEventCost = NonNegativeNumeric.tryCreate(500L),
  )
  sequencer1.topology.synchronizer_parameters.propose_update(
    synchronizerId = sequencer1.synchronizer_id,
    _.update(trafficControl = Some(trafficControlParameters)),
  )

Let’s confirm that the traffic management is now enabled by checking the synchronizer parameters again:

@ sequencer1.topology.synchronizer_parameters
  .get_dynamic_synchronizer_parameters(sequencer1.synchronizer_id)
res3: DynamicSynchronizerParameters = DynamicSynchronizerParameters(
  confirmation response timeout = 30s,
  mediator reaction timeout = 30s,
  assignment exclusivity timeout = 1m,
  topology change delay = 0.25s,
  ledger time record time tolerance = 1m,
  mediator deduplication timeout = 48h,
  reconciliation interval = 1m,
  confirmation requests max rate = 1000000,
  max request size = 10485760,
  sequencer aggregate submission timeout = 6m,
  traffic control = TrafficControlParameters(
    max base traffic amount = 20000,
    read vs write scaling factor = 200,
    max base traffic accumulation duration = 10s,
    set balance request submission window size = 5m,
    enforce rate limiting = true,
    base event cost = 500
  ),
  ACS commitment catchup = AcsCommitmentsCatchUpParameters(
    catch up interval skip = 5,
    number of intervals to trigger catch up = 2
  ),
  participant synchronizer limits = ParticipantSynchronizerLimits(
    confirmation requests max rate = 1000000
  ),
  preparation time record time tolerance = 24h,
  onboarding restriction = UnrestrictedOpen
)

Note the traffic control in the output above.

Check the latest traffic balances of Synchronizer members

One can interactively inspect and modify the traffic balances of Synchronizer members using the Canton console commands under sequencer.traffic_control.

To inspect the traffic balances of all members of a Synchronizer, you can use the following command:

@ val allMembersTrafficState = sequencer1.traffic_control.traffic_state_of_all_members()
  allMembersTrafficState
allMembersTrafficState : com.digitalasset.canton.synchronizer.sequencer.traffic.SequencerTrafficStatus = SequencerTrafficStatus(
  trafficStatesOrErrors = Map(
    PAR::participant1::12201ff69b1d... -> Right(
      value = TrafficState(
        extraTrafficLimit = 0,
        extraTrafficConsumed = 0,
        baseTrafficRemainder = 20000,
        lastConsumedCost = 0,
        timestamp = 2025-07-17T22:19:58.353490Z,
        availableTraffic = 20000
      )
    ),
    MED::mediator1::122009299340... -> Right(
      value = TrafficState(
        extraTrafficLimit = 0,
        extraTrafficConsumed = 0,
        baseTrafficRemainder = 20000,
        lastConsumedCost = 0,
        timestamp = 2025-07-17T22:19:58.353490Z,
        availableTraffic = 20000
      )
    ),
    PAR::participant3::1220d6908163... -> Right(
      value = TrafficState(
        extraTrafficLimit = 0,
        extraTrafficConsumed = 0,
        baseTrafficRemainder = 20000,
        lastConsumedCost = 0,
        timestamp = 2025-07-17T22:19:58.353490Z,
        availableTraffic = 20000
      )
    ),
    PAR::participant2::1220a4d7463b... -> Right(
      value = TrafficState(
        extraTrafficLimit = 0,
        extraTrafficConsumed = 0,
        baseTrafficRemainder = 20000,
        lastConsumedCost = 0,
        timestamp = 2025-07-17T22:19:58.353490Z,
        availableTraffic = 20000
      )
    )
  )
)

If you only want to check the traffic balance of a specific member, you can use:

@ sequencer1.traffic_control.traffic_state_of_members(Seq(participant1))
res5: com.digitalasset.canton.synchronizer.sequencer.traffic.SequencerTrafficStatus = SequencerTrafficStatus(
  trafficStatesOrErrors = Map(
    PAR::participant1::12201ff69b1d... -> Right(
      value = TrafficState(
        extraTrafficLimit = 0,
        extraTrafficConsumed = 0,
        baseTrafficRemainder = 20000,
        lastConsumedCost = 0,
        timestamp = 2025-07-17T22:19:58.353490Z,
        availableTraffic = 20000
      )
    )
  )
)

Top up the traffic balance for a Synchronizer member

Note

Traffic balance entitlements of members are decided by an external workflow and communicated via submitting the same traffic balance via one or multiple Sequencers.

Note

Top ups must be submitted by a quorum of Sequencers to become effective. This is configured by the threshold parameter of the SequencerSynchronizerState topology mapping.

Let’s add some traffic for a member, for example, participant1. First we need to know the current serial (a per-member monotonically increasing PositiveInt), which corresponds to the last traffic balance top up for that member.

@ val nextSerial = allMembersTrafficState.trafficStates(participant1).serial
  .getOrElse(PositiveNumeric.tryCreate(1))
  .increment
nextSerial : PositiveNumeric[Int] = PositiveNumeric(value = 2)

Now we can submit a command to increase the traffic balance for participant1 by newBalance:

@ sequencer1.traffic_control.set_traffic_balance(
    member = participant1,
    serial =  nextSerial,
    newBalance = NonNegativeNumeric.tryCreate(1000000L),
  )

Now the traffic balance for participant1 has been updated. You can verify this by checking the traffic state again:

@ utils.retry_until_true(
    sequencer1.traffic_control.traffic_state_of_members(Seq(participant1))
      .trafficStates(participant1)
      .serial
      .exists(_ >= nextSerial)
  )
@ val trafficStateBeforePing = sequencer1.traffic_control.traffic_state_of_members(Seq(participant1))
  trafficStateBeforePing
trafficStateBeforePing : com.digitalasset.canton.synchronizer.sequencer.traffic.SequencerTrafficStatus = SequencerTrafficStatus(
  trafficStatesOrErrors = Map(
    PAR::participant1::12201ff69b1d... -> Right(
      value = TrafficState(
        extraTrafficLimit = 1000000,
        extraTrafficConsumed = 0,
        baseTrafficRemainder = 20000,
        lastConsumedCost = 0,
        timestamp = 2025-07-17T22:19:58.861158Z,
        serial = 2,
        availableTraffic = 1020000
      )
    )
  )
)

Now let’s run ping between participants and observe the traffic consumption:

@ participant1.health.ping(participant2)
res10: Duration = 923 milliseconds
@ participant2.health.ping(participant3)
res11: Duration = 778 milliseconds
@ sequencer1.traffic_control.traffic_state_of_all_members()
res12: com.digitalasset.canton.synchronizer.sequencer.traffic.SequencerTrafficStatus = SequencerTrafficStatus(
  trafficStatesOrErrors = Map(
    PAR::participant1::12201ff69b1d... -> Right(
      value = TrafficState(
        extraTrafficLimit = 1000000,
        extraTrafficConsumed = 0,
        baseTrafficRemainder = 17853,
        lastConsumedCost = 0,
        timestamp = 2025-07-17T22:20:00.709255Z,
        serial = 2,
        availableTraffic = 1017853
      )
    ),
    MED::mediator1::122009299340... -> Right(
      value = TrafficState(
        extraTrafficLimit = 0,
        extraTrafficConsumed = 0,
        baseTrafficRemainder = 19358,
        lastConsumedCost = 642,
        timestamp = 2025-07-17T22:20:00.709255Z,
        availableTraffic = 19358
      )
    ),
    PAR::participant3::1220d6908163... -> Right(
      value = TrafficState(
        extraTrafficLimit = 0,
        extraTrafficConsumed = 0,
        baseTrafficRemainder = 16127,
        lastConsumedCost = 0,
        timestamp = 2025-07-17T22:20:00.709255Z,
        availableTraffic = 16127
      )
    ),
    PAR::participant2::1220a4d7463b... -> Right(
      value = TrafficState(
        extraTrafficLimit = 0,
        extraTrafficConsumed = 0,
        baseTrafficRemainder = 12761,
        lastConsumedCost = 0,
        timestamp = 2025-07-17T22:20:00.709255Z,
        availableTraffic = 12761
      )
    )
  )
)

Observe the traffic balances for the Participants and the Mediator decrease.