Skip to content

Loss Injection & CUBIC Congestion Control

The Java media driver ships an extension package — io.aeron.driver.ext — that holds two operationally interesting things that the default driver does not turn on for you:

  1. Debug channel endpoints with loss generators — inject packet loss on purpose, so you can measure recovery before production does the experiment for you.
  2. CUBIC congestion control — replace Aeron’s fixed receiver window with one that backs off on loss and probes back up, for shared or long-fat networks.

Neither is wired in by default. This page is the operational side — how to switch them on, what the knobs are, and how they move p50 / p99 / throughput. For how Aeron detects gaps, retransmits, and applies flow control internally, defer to The Aeron Files.

Part 1 — Loss injection for chaos testing

Section titled “Part 1 — Loss injection for chaos testing”

The two-step wiring (the part that’s easy to miss)

Section titled “The two-step wiring (the part that’s easy to miss)”

The loss rate is controlled by a system property, but setting the property alone does nothing. The default channel endpoints don’t consult a loss generator at all. You first have to swap in the debug endpoints, which is a separate property:

So a 10% receive-side data-loss driver needs both properties:

Terminal window
java \
-Daeron.ReceiveChannelEndpoint.supplier=io.aeron.driver.ext.DebugReceiveChannelEndpointSupplier \
-Daeron.debug.receive.data.loss.rate=0.10 \
-Daeron.debug.receive.data.loss.seed=42 \
io.aeron.driver.MediaDriver

DebugSendChannelEndpointSupplier is the symmetric switch for the send side. Set whichever side(s) the loss knobs you’re using actually apply to.

These are read by DebugChannelEndpointConfiguration. Each rate is a probability in [0.0, 1.0]; each has a matching seed.

PropertyWhat it dropsPerspective
aeron.debug.receive.data.loss.rate / .seedDataReceiver inbound — data arriving at the receiver
aeron.debug.receive.control.loss.rate / .seedControl (SM / NAK)Receiver outbound — control leaving the receiver
aeron.debug.send.data.loss.rate / .seedDataSender outbound — data leaving the sender
aeron.debug.send.control.loss.rate / .seedControl (SM / NAK)Sender inbound — control arriving at the sender

Defaults: every rate defaults to 0.0 (no loss), every seed to -1.

Two facts that decide whether your test is meaningful

Section titled “Two facts that decide whether your test is meaningful”

The rate path uses RandomLossGenerator — and only that. DebugChannelEndpointConfiguration.lossGeneratorSupplier(rate, seed) short-circuits to a no-op generator when rate == 0, and otherwise returns a RandomLossGenerator. Its decision is literally random.nextDouble() <= lossRate per frame. The package also contains FixedLossGenerator and MultiGapLossGenerator — but the system-property mechanism never instantiates them; they’re for programmatic test setups (e.g. JavaTestMediaDriver). So:

  • The rate knobs give you uniform random loss, not a scripted gap pattern.
  • Because the dice are rolled on every frame, random loss will also drop retransmissions at the same rate. That’s realistic for a lossy link — but it means a high rate can stack retransmits. (The scripted FixedLossGenerator / MultiGapLossGenerator, by contrast, drop a frame only once and let its retransmission through. If you need that, you need the programmatic path, not these properties.)

The seed decides reproducibility. seed == -1 (the default) constructs new Random() — a different loss pattern every run, which makes a flaky failure impossible to bisect. Pin any non-negative seed and the pattern is identical run to run, so the same seed means the same test outcome — the prerequisite for a valid A/B (default timers vs derived timers) and for CI.

  • Throughput takes the most direct hit: retransmits compete with fresh data for the same bandwidth, and the cost climbs faster than the loss rate once retransmissions start to stack.
  • p99 / the tail stretches first. A dropped frame stalls the subscriber until detection + retransmission complete; that stall is governed by the NAK/retransmit timers, which is exactly why NAK timer tuning is the lever that collapses the loss tail.
  • p50 is comparatively stable at low loss rates — most messages arrive first try — and only degrades once loss is high enough to hit the median.

Use a fixed seed when measuring any of these, or the run-to-run variance swamps the effect you’re trying to read.

By default an Aeron receiver advertises a fixed window (StaticWindowCongestionControl, the cc=static strategy): the receiver window never changes in response to loss. On a dedicated LAN sized to your BDP that’s ideal — no needless backoff. On a shared or long-fat link it isn’t: a fixed large window keeps shoving data into a congested path, inflating queueing loss and the tail.

CubicCongestionControl replaces that with a window that shrinks on loss and grows back along a cubic curve — the same family as Linux TCP’s default. It’s a receiver-side controller: it manipulates the receiver window length that drives status messages.

CUBIC is selected per channel via the cc URI param — no driver-wide property needed, because the DefaultCongestionControlSupplier already understands it:

// Per publication/subscription channel:
aeron:udp?endpoint=192.168.1.10:40456|cc=cubic

cc=static (or omitting cc) keeps the fixed-window default; cc=cubic opts that stream into CUBIC. To force every stream onto CUBIC driver-wide instead, set the supplier:

Terminal window
-Daeron.CongestionControl.supplier=io.aeron.driver.ext.CubicCongestionControlSupplier

Read by CubicCongestionControlConfiguration:

PropertyDefaultEffect
aeron.CubicCongestionControl.measureRttfalseWhen true, measure RTT live (RTT probes) instead of using a static estimate
aeron.CubicCongestionControl.initialRtt100usThe RTT estimate used when not measuring; also the floor for the RTT-timeout
aeron.CubicCongestionControl.tcpModefalseAdd the TCP-friendly window term so CUBIC won’t undershoot TCP after a loss in the low-RTT region

The algorithm’s own constants are fixed in code: scaling C = 0.4, multiplicative-decrease B = 0.2, and an initial congestion window INITCWND = 10 MTUs.

How it reacts (straight from the controller)

Section titled “How it reacts (straight from the controller)”
  • On loss: remember the current window as w_max, then cut the window to cwnd × (1 − B) — a 20% reduction (floored at 1 MTU). This is the backoff a static window can’t do.
  • Recovery: the window climbs back along W_cubic = C·(T − K)³ + w_max, where K = cbrt(w_max · B / C) is the time to return to w_max. Growth is slow near w_max and faster away from it — the cubic shape. (The class comment works an example: MTU 4 KB, max window 128 KB ⇒ K ≈ 2.5 s.)
  • tcpMode on: additionally compute the TCP-equivalent window and take the max, so CUBIC stays at least as aggressive as TCP would in the low-RTT-after-loss region.

CUBIC exposes two per-image counters you can watch live with aeron-stat: rcv-cc-cubic-wnd (the current window it’s advertising) and rcv-cc-cubic-rtt (its RTT estimate). Watching the window sawtooth up and collapse on loss is the fastest way to confirm it’s actually engaged.

cc=static (default)cc=cubic
Best onDedicated LAN sized to BDPShared / WAN / long-fat links
On lossWindow unchanged — keeps pushingWindow cut 20%, cubic recovery
ThroughputHighest on a clean linkSlightly lower on a clean link; higher on a congested one (avoids collapse)
Tail (p99)Can balloon if a fixed-large window overruns a congested pathTighter under congestion; backoff caps queueing

The rule of thumb: clean dedicated link → keep static and size the window to BDP; contended or long-fat link → try cubic and watch rcv-cc-cubic-wnd against your throughput target.

These two halves of ext compose: drive a stream with cc=cubic, inject a fixed-seed loss rate via the debug endpoints, and watch rcv-cc-cubic-wnd react. That’s a repeatable bench for how your congestion control behaves under known loss — the kind of test that’s impossible to run honestly without both the deterministic loss source and the dynamic window in the same run.

Appendix — the C driver injects loss differently

Section titled “Appendix — the C driver injects loss differently”

The C media driver (aeronmd) does not use the Java ext properties. It injects loss through a UDP transport interceptor, configured by environment variables:

Terminal window
export AERON_UDP_CHANNEL_INCOMING_INTERCEPTORS="loss"
export AERON_UDP_CHANNEL_TRANSPORT_BINDINGS_LOSS_ARGS="rate=0.2|recv-msg-mask=0xF"
aeronmd

The args are pipe-separated key=value pairs: rate (probability, parsed as a double), seed (unsigned), and recv-msg-mask (a hex bitmask selecting which inbound message types are subject to loss). See Media Driver Selection: Java vs C for when you’d be running the C driver in the first place.