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:
- Debug channel endpoints with loss generators — inject packet loss on purpose, so you can measure recovery before production does the experiment for you.
- 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:
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.MediaDriverDebugSendChannelEndpointSupplier is the symmetric switch for the send side. Set whichever side(s)
the loss knobs you’re using actually apply to.
The four loss knobs
Section titled “The four loss knobs”These are read by DebugChannelEndpointConfiguration. Each rate is a probability in [0.0, 1.0];
each has a matching seed.
| Property | What it drops | Perspective |
|---|---|---|
aeron.debug.receive.data.loss.rate / .seed | Data | Receiver inbound — data arriving at the receiver |
aeron.debug.receive.control.loss.rate / .seed | Control (SM / NAK) | Receiver outbound — control leaving the receiver |
aeron.debug.send.data.loss.rate / .seed | Data | Sender outbound — data leaving the sender |
aeron.debug.send.control.loss.rate / .seed | Control (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.
What loss does to p50 / p99 / throughput
Section titled “What loss does to p50 / p99 / throughput”- 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.
Part 2 — CUBIC congestion control
Section titled “Part 2 — CUBIC congestion control”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.
Turning it on
Section titled “Turning it on”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=cubiccc=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:
-Daeron.CongestionControl.supplier=io.aeron.driver.ext.CubicCongestionControlSupplierThe three CUBIC knobs
Section titled “The three CUBIC knobs”Read by CubicCongestionControlConfiguration:
| Property | Default | Effect |
|---|---|---|
aeron.CubicCongestionControl.measureRtt | false | When true, measure RTT live (RTT probes) instead of using a static estimate |
aeron.CubicCongestionControl.initialRtt | 100us | The RTT estimate used when not measuring; also the floor for the RTT-timeout |
aeron.CubicCongestionControl.tcpMode | false | Add 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 tocwnd × (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, whereK = cbrt(w_max · B / C)is the time to return tow_max. Growth is slow nearw_maxand faster away from it — the cubic shape. (The class comment works an example: MTU 4 KB, max window 128 KB ⇒K ≈ 2.5 s.) tcpModeon: 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.
Static vs CUBIC — when each wins
Section titled “Static vs CUBIC — when each wins”cc=static (default) | cc=cubic | |
|---|---|---|
| Best on | Dedicated LAN sized to BDP | Shared / WAN / long-fat links |
| On loss | Window unchanged — keeps pushing | Window cut 20%, cubic recovery |
| Throughput | Highest on a clean link | Slightly lower on a clean link; higher on a congested one (avoids collapse) |
| Tail (p99) | Can balloon if a fixed-large window overruns a congested path | Tighter 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.
Combining the two
Section titled “Combining the two”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:
export AERON_UDP_CHANNEL_INCOMING_INTERCEPTORS="loss"export AERON_UDP_CHANNEL_TRANSPORT_BINDINGS_LOSS_ARGS="rate=0.2|recv-msg-mask=0xF"aeronmdThe 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.
This site is not affiliated with, endorsed by, or sponsored by Adaptive Financial Consulting Limited or the Aeron project. Aeron is a registered trademark of Adaptive Financial Consulting Limited.
Aeron is a trademark of Adaptive Financial Consulting Limited in the United Kingdom and other countries.