Skip to content

Reading the Aeron® Benchmark Honestly

The Aeron® benchmark is a good instrument. The mistakes are almost always in how we read it. The most common one: quoting a latency-at-a-chosen-rate figure as if it were a capacity figure. Those are different claims, and the gap between them is where teams fool themselves.

Three specific misreadings this page exists to correct:

  1. Reading “benchmark peak” as “message cap.” The benchmark never searches for the cap. You tell it the rate; it tells you the latency. The real cap — the knee, where latency crosses your SLA — it never measured.
  2. Quoting burst=10 numbers as if production would see them. The burst knob pre-coalesces traffic into a best case that does not exist in production — nobody ships a “batch by 10” knob on a live feed. Good numbers are partly an artifact of that knob.
  3. Repeating “DPDK is ~60× / 200× faster than the Java driver.” That ratio compares a healthy DPDK point against a saturated Java point. Run both below their knee and the gap collapses to a small factor — and AWS’s own 2025 benchmark shows the same thing (see DPDK myth).

None of this means the benchmark is broken or that runs are wasted. The instrument is fine. The risk is over-claiming from it.

The cluster benchmark (remote-cluster-benchmarks) and MDC benchmark (remote-echo-mdc-benchmarks) are closed-loop latency harnesses. A load generator:

  1. Publishes messages into the cluster (or MDC publication) at a fixed target rate you specify (--rate).
  2. Stamps each message with clock.nanoTime() at offer time.
  3. Measures round-trip latency (ingress → consensus/commit → service → egress) into an HdrHistogram.
  4. Reports P50 / P99 / P99.9 / Max at that rate and message size.

It is a stopwatch, not a capacity meter. It answers “at rate R, size S, MTU M, arrival-shape B, what is the latency distribution?” — a precise, reproducible question. It does not directly answer “what’s the most this box can take?”

It testsIt does not test
Latency at a chosen rate/sizeThe true saturation cap for your traffic
A synthetic arrival process (paced + batched)Real bursty / conflatable production arrivals
The transport + consensus hot pathYour app’s business logic, GC, serialization
One message size at a timeMixed-size real traffic
Steady state over a fixed windowCold start, failover, market-open spikes

Read every benchmark row as a single sentence with all its qualifiers, never just the number:

If I drive this exact setup at rate R, message size S, MTU M, arrival shape burst=B, over a steady window, then the round-trip latency distribution was P50/P99/P99.9 = … and X% of messages arrived.”

Everything before “then” is an input you chose. The number is a response to those inputs, not a property of the box.

The benchmark IS a good tool for:

  • Relative ranking of silicon / NIC / MTU / driver when every input is held fixed (c8a vs c8i vs c8g at the same R/S/B/MTU). This is its strongest, most trustworthy use.
  • Finding the knee if you sweep the rate yourself at your real size and arrival shape: the rate where P99.9 crosses your SLA is the usable ceiling for that traffic.
  • Regression detection — did a code/config change move latency at a fixed operating point?
  • Tail characterization under a specified load — where NAK/retransmit/queueing show up.
  • A reproducible, independently verifiable apples-to-apples comparison (which is exactly why the harness fixes the rate — see AWS’s stated methodology below).

The benchmark is NOT:

  • Not a capacity/throughput cap. “1M, ✅” ≠ “the cap is 1M.” The cap is the knee, which the row didn’t find.
  • Not a production latency prediction unless your arrival shape, size, and MTU match the run. A burst=10, 32B headline will not reproduce on an evenly-paced, 470B production feed.
  • Not a driver “speed multiplier.” The DPDK-vs-Java gap is not a constant × ; it’s a difference in knee position and tail, which looks huge only when one side is already saturated.
  • Not a full-system number. It excludes your business logic, serialization, GC, disk, failover — all of which land in the real end-to-end latency your users see.
  • Not a guarantee under bursts. Steady-state P50 says nothing about a market-open spike; look at Max/P99.9 under a bursty pattern for that.

The one-line rule: the benchmark tells you latency for a traffic shape you invented; production tells you the traffic shape. Never quote the former as capacity for the latter.

BURST_SIZE-Dio.aeron.benchmarks.batch.size in the load generator. It is not an Aeron code path — Aeron’s offer() and the driver’s TermScanner are byte-for-byte identical regardless. The knob changes only the offer cadence:

batch.size = N → offer N messages back-to-back, THEN wait for the next rate tick.
  • batch.size=1 (even): offer → wait → offer → wait. The media-driver Sender drains each frame before the next arrives → 1 message per datagram → the receiver pays the full per-datagram (packets-per-second) cost → it saturates early.
  • batch.size=10 (burst): ten offers with no gap → the ten frames co-reside in the term buffer → TermScanner.scanForAvailability() coalesces them into one datagram ≤ MTU → the receiver does ~1/10 the per-datagram work → the same rate sails through.

The coalescing mechanism is 100% real in production. The knob that forces a fixed bunch of 10 is benchmark-only. So burst=10 numbers are a best-case, pre-coalesced measurement. Quote a burst=10 “peak” as capacity while your production feed arrives evenly, and you will be wrong by a large factor.

One knob turned a 5-millisecond disaster into 56 microseconds

Section titled “One knob turned a 5-millisecond disaster into 56 microseconds”

This is not hypothetical. A deliberately rigorous A/B was run to settle exactly this question — same 4-node bed (c8i.8xlarge, single-AZ placement group, isolated/pinned cores, C-states off), same 1M msg/s × 32B cluster workload, MTU 8192 on every arm, interleaved (one full cycle of all cells per rep, ×3) so no session drift could masquerade as a result. The only thing that changed was the BURST_SIZE knob.

The Java media driver, burst=1 (evenly paced) — a saturated rig:

MetricResult
P504,698 – 7,737 µs
P9913,214 – 15,385 µs
P99.913,623 – 16,974 µs
Maxup to 20,677 µs
StateSATURATED — packet-rate wall

Five to eight milliseconds at the median, a ~14 ms P99 tail. As a message-transport that is a non-starter — the single receiver thread cannot dispatch 1M individual datagrams/s, so the intake queue runs away.

Same rig, same code, same hardware — flip BURST_SIZE=1 → 10:

MetricResult
P5056.4 – 125.5 µs
P9972.8 – 188.2 µs
P99.990.0 – 197.5 µs
Max267 – 505 µs
Statede-saturated — sustains the full 1M/s

The median collapsed from ~5–8 ms to ~56 µs — a ~60–130× improvement, into sub-100 µs at its best rep, with the P99.9 tail down from ~14 ms to ~90–197 µs. No faster NIC. No DPDK. No extra cores. No code change. One knob that changes when the client calls offer(). At burst=10 roughly ten 32 B frames coalesce into each datagram, so the leader’s receiver does ~1/10 the per-datagram work and clears the wall it was drowning at.

2b. MTU: when it helps, and when it does nothing

Section titled “2b. MTU: when it helps, and when it does nothing”

MTU (AERON_DPDK_MTU, or the channel mtu/term config) is the size of the box that coalescing packs into. A jumbo MTU (8192 / 9001) is only useful if there is something to pack. This is the most misunderstood knob, because MTU and burst are multiplicative — MTU sets the ceiling on coalescing; burst decides whether you reach it.

TermScanner.scanForAvailability() packs as many resident frames as fit in min(senderWindow, MTU) into one datagram. So the coalescing ratio is bounded by two independent limits:

  • MTUhow many frames could fit (a ceiling).
  • Burst / arrival shapehow many frames are actually resident when the sender scans (the supply).

You get min(what fits, what's present). A bigger box does nothing if you only ever hand it one item.

Case A — burst=1 (even, one frame resident): MTU barely matters. With evenly-paced offers, the Sender drains each frame before the next arrives, so only ~1 frame is resident per scan. One frame goes in the datagram regardless of MTU — an 8192-byte MTU carrying a single 32B frame is 32 bytes on the wire, exactly like a 1408-byte MTU would be. Raising MTU does not reduce your datagram (PPS) rate in the even case, because the datagram count is set by message count, not MTU. This is why “just turn on jumbo frames” often does nothing for an evenly-paced feed’s saturation. (MTU can even slightly hurt the even-case latency tail: larger buffers = marginally more buffering.)

Case B — burst=10 / continuous (many frames resident): MTU is a real lever. Now many frames co-reside, so the datagram is packed up to the MTU limit. MTU directly sets the coalescing ratio, and it scales with message size vs MTU:

Msg sizeFrames per 1408B MTUFrames per 8192B MTUFrames per 9001B MTU
32B~40~230~250
288B~4–5~26~29
1344B1 (barely fits)~6~6

(Approximate — subtract Aeron/UDP/IP framing overhead; frames are 32-byte aligned.)

Two things fall out of that table:

  • At 32B, burst, MTU is huge: 40:1 vs 250:1 datagram reduction. Jumbo frames massively raise the ceiling.
  • At 1344B, a 1408B MTU can’t coalesce at all (one frame barely fits), so burst gives you nothing until you also raise MTU. This is exactly why 1344B rows can “saturate” at 1M even on DPDK with MTU 1408 but pass at 8192 or lower rates — MTU, not burst, was the missing lever there.

The trap: a great headline number usually has both knobs turned to best-case: small messages + big MTU + burst. That’s 32B / 8192 / burst=10 → ~250:1 coalescing → the per-datagram ceiling is 250× easier to stay under. Production with larger messages, default MTU, or even arrival hits none of that multiplier and saturates far sooner.

Rule of thumb:

  • Even (burst=1) traffic: MTU is nearly irrelevant to your datagram/PPS ceiling — don’t expect jumbo frames to save you. Fix arrival shape (micro-batch) or raise the ceiling (DPDK).
  • Bursty / continuous traffic: MTU is a first-class lever, and its benefit scales with how small your messages are relative to the MTU. Match the benchmark MTU and message size to production or the coalescing ratio (and thus the peak) won’t transfer.

2c. How to formulate “burst” — the correct way

Section titled “2c. How to formulate “burst” — the correct way”

So far “burst” has meant the benchmark’s batch.size knob. But when you size a real system, burst=10 is the wrong unit entirely. The benchmark knob is “how many messages my one client offers back-to-back.” The production question is the opposite end of the pipe: “how many messages can land at the receiver within one drain interval, and can my buffers absorb that clump without dropping?” Those are different quantities, and only the second one predicts the tens-of-milliseconds tails you actually see in the field.

Burst is a COUNT of coincident arrivals, not a rate

Section titled “Burst is a COUNT of coincident arrivals, not a rate”

The single biggest real-world burst source is N independent producers firing at the same instant — not one client’s cadence. A production incident is the textbook case:

  • 8 replay streams, each a stable long-lived session, happened to be frame-aligned and all burst at once.
  • Baseline 45.9 pkts/ms; the superimposed spike hit 658 pkts/ms — 17.2× baseline.
  • The single Aeron receiver thread drains serially, so 57,165 packets backed up in the OS socket buffer in a 211 ms window → 57.7 ms worst-case latency, 45,259 slow events.

The network was fine (19 µs transit, p99 28.5 µs). Nothing was “too slow.” N sources coincided, the clump exceeded what one drain cycle could clear, and it queued. That is what “burst” means in production: a superposition, measured in packets (or bytes) arriving inside one service interval. This is the receiver-side view developed in Benchmarking Micro-Bursts.

Model burst as the queue the receiver must hold when arrivals momentarily exceed drain:

Peak arrival rate λ_peak = Σ (per-source peak rate) [msgs/s] ← superpose ALL sources
Drain rate μ = receiver msgs/s it can dispatch [msgs/s] ← the knee
Burst duration T_b = how long the clump lasts [s] ← from your trace / alignment
Backlog to absorb Q_burst = max(0, (λ_peak − μ) × T_b) [msgs] ← THE number that matters
Buffer must cover B_bytes ≥ Q_burst × avg_wire_bytes_per_msg [bytes]
Worst-case queueing lat L_q ≈ Q_burst / μ [s] ← e.g. 57k / (1M/s) ≈ 57 ms
  • λ_peak is a sum, not one stream. With N synchronized sources each peaking at r, λ_peak ≈ N·r. The incident spike = 8 sources superposing to 17× baseline. Always superpose; a per-source rate lies.
  • Q_burst is the deliverable. It is the number of messages that pile up while λ_peak > μ. If your socket buffer / Aeron receiver window / term buffer can’t hold Q_burst, you drop → NAK → retransmit → the multi-ms tail. This is the direct link to buffer sizing: size the buffer to Q_burst, not to the average rate. See receive-path buffers and the buffer sizing calculator.
  • L_q is the tail you will actually measure. 57,165 msgs ÷ ~1M msgs/s ≈ 57 ms — the formula reproduces the observed incident. That is the number to put in front of the product team, not the benchmark median.

Two separate budgets — don’t conflate them

Section titled “Two separate budgets — don’t conflate them”

There are two “burst capacities” and they are set by different things:

  1. The transport/flow-control burst budget (sender side). On a NIC egress limiter (AWS token-bucket), there is a fixed bits burst allowance above baseline (e.g. a bucket 1 ms deep = 5,000,000 bits on a 5 Gbps instance), and recovery time = burst ÷ spare-bits-per-ms. This budget is in bits, MTU-independent; MTU only changes how many messages fit that fixed bit budget (the 32 B Aeron frame header is per-message, un-amortizable, so small single-frame messages barely benefit). The sender-side queue this builds is covered in sender queueing and burst pacing.
  2. The receiver absorption budget (receive side). Q_burst above — bounded by socket buffer + receiver window + drain rate μ. This is what breaks in the incident above.

Your effective burst tolerance is the minimum of the two. A generous 5 Gbps egress allowance is useless if the receiver’s single-thread drain + socket buffer can’t absorb the resulting clump — which is exactly what happened.

Not batch.size. It is Q_burst = (λ_peak − μ)·T_b, where λ_peak superposes all coincident producers, μ is the receiver’s measured drain (the knee), and T_b is the clump duration from your trace. Size socket buffer / receiver window / term buffer ≥ Q_burst × wire-bytes/msg, keep λ_peak under the minimum of the sender bit-budget and the receiver absorption budget, and quote L_q ≈ Q_burst/μ as your worst-case tail. The levers to shrink Q_burst: raise μ (split subscriptions across K≥2 receiver threads/drivers, DPDK), or lower λ_peak/T_b (de-synchronize producers, stagger/jitter their sends so N sources don’t frame-align).

3. Governor vs open-loop — and what production actually is

Section titled “3. Governor vs open-loop — and what production actually is”

The benchmark’s rate governor (the “wait for the next tick”) exists so runs are comparable. It does not exist in production — you don’t get to tell the exchange to send market data at 300K/s.

Production is open-loop: you offer() as data arrives. Regulation comes not from a clock but from back-pressure (offer() returns < 0 / BACK_PRESSURED when the publication window is full). The real production design decision is your overflow policy, which the benchmark never exercises:

PolicyWhen it’s rightCost
Block / spin-retryLossless-required (order entry, sequenced commands)Back-pressure must go upstream; else unbounded latency tail
Conflate / drop-to-latestMarket data / state snapshots (only freshest matters)Loses intermediate values (usually fine for feeds)
Bounded buffer + policyAbsorb micro-bursts, apply policy only when fullPragmatic default

“Removing the sleep” ≠ “burst=10.” It removes rate control and runs open-loop; coalescing then becomes an emergent side-effect of saturation (capped by MTU, often more than 10:1), and any queueing delay lands in your measured latency. That’s a throughput experiment, not a controlled-rate latency one.

4. What a production-representative benchmark looks like

Section titled “4. What a production-representative benchmark looks like”

To make a benchmark predictive of your deployment, stop optimizing the headline and start matching reality:

  1. Match the arrival process, not just the rate.
    • Best: replay a captured production trace (real inter-arrival gaps and bursts).
    • Otherwise: model the burstiness. Do not default to burst=10 because it looks good.
  2. Report burst=1 (even) as your conservative floor, and burst=10 as “achievable if traffic is bursty or you add app-level micro-batching.” Quote both, labeled.
  3. Match message size AND MTU to production. Coalescing ratio scales with size:MTU (32B into 1408B ≈ 40:1; 470B ≈ 3:1; 1344B may not coalesce at all).
  4. Test the tail, not just the median. P99.9 / Max under a bursty pattern is where NAK/retransmit and queue build-up show up — that’s your real risk, not P50.
  5. If production is evenly-paced and you need the higher number, that’s design work: add a bounded micro-batch in the client (offer up to N messages or T µs, whichever first — cap by both so a lull can’t stall a message). Then benchmark that client, not the synthetic knob.
  6. Include cold paths relevant to your SLA: startup, leader failover/RESIGN, snapshot, market-open spike.
  7. Measure at your target ρ, below the knee. Size so normal micro-bursts never hit back-pressure; treat hitting it as an overload signal, not steady state.

Preparing your own load — even with NO historical data

Section titled “Preparing your own load — even with NO historical data”

The single most valuable thing you can do is stop feeding the benchmark a synthetic shape and feed it your shape.

If you HAVE production traffic — build a replay:

  1. Capture the arrival timestamps, not just the count. What matters is the inter-arrival distribution and burst structure. Sources: app-level publish timestamps (best — same layer as offer()), a pcap/tcpdump of the ingress stream, or feed-handler logs with per-message receive times.
  2. Capture the message-size distribution too (small heartbeats vs large snapshots). A size histogram beats a single average.
  3. Replay it faithfully from the captured (timestamp, size) trace — do not collapse it to a fixed --rate + burst. If your harness can’t replay a trace, at minimum fit the trace to a rate and a realistic burst factor and run that, labeled as an approximation.
  4. Scale honestly. If you expect 3× growth, replay at 3× (compress inter-arrival times), but keep the burst structure — don’t just raise a flat rate, which erases the bursts that actually break you.
  5. Include the worst window, not the average day. Replay your market-open / failover / retry-storm minute.

If you’re a NEW project with NO data to play back, bracket the truth instead of guessing a point:

  1. Run the two ends of the envelope, always both: burst=1 (evenly paced) = your pessimistic floor (meet your SLA here and you’re safe for any arrival shape at that rate); burst=high / open-loop = your optimistic ceiling. Real production lands between them. Size off the floor, not the ceiling.
  2. Find the knee, don’t assume the headline. Sweep --rate at burst=1 and your best-guess message size, and record where P99.9 crosses your SLA. That knee is your defensible capacity number.
  3. Estimate arrivals from first principles: take your business peak (orders/sec at open, ticks/sec for the busiest symbol), multiply by a safety factor (2–3× is common for financial peaks), and assume bursty until proven otherwise — real event streams clump, they aren’t Poisson-smooth. Get message size from your schema (encode a representative SBE/proto message and measure its wire size), not a guess.
  4. Provision for headroom, then measure into it. Target ρ < ~0.7 at your estimated peak, launch, and instrument production from day one (publish timestamps, offer() back-pressure count, driver cycle-time thresholds) so your guessed model gets replaced by a real trace within weeks.
  5. Decide your overflow policy before launch, not during the first incident — lossless-block vs conflate-drop vs bounded-buffer. With no data, what happens when you’re wrong about the rate matters more than the rate estimate itself.

The mindset shift: with no history, your job is not to produce one impressive number — it’s to produce a safe floor (burst=1 knee) you can defend, plus a plan to replace your assumptions with real data fast.

5. Using benchmark results to pick an EC2 spec

Section titled “5. Using benchmark results to pick an EC2 spec”

The benchmark is genuinely useful for relative comparison — silicon, NIC, MTU, driver — as long as you hold the arrival shape fixed and read it as relative, not absolute.

Do:

  • Compare boxes at the same rate/size/burst/MTU to rank silicon (e.g. c8a vs c8i vs c8g) — the benchmark is reliable for ordering.
  • Find the knee for each spec: sweep --rate at your production message size and burst=1 (or your trace), and note where P99.9 crosses your SLA. That knee — not the headline P50 — is the spec’s usable ceiling for that traffic.
  • Size for headroom: pick the spec whose knee sits comfortably above your peak arrival rate (target ρ < ~0.7), so bursts have somewhere to go.
  • Weight the tail by your SLA. A box with a lower P50 but a fatter P99.9 under bursts may be worse for a latency-sensitive path.
  • Account for the whole box. Cluster needs cores for conductor/sender/receiver/archive/consensus/service; a “faster” spec that forces cross-NUMA or HT contention can lose.

Don’t:

  • Don’t size off a burst=10, 32B, 1M headline unless your production is genuinely that bursty and that small. It’s a best case.
  • Don’t read “Saturated” as “the box is too slow.” It usually means this rate at this size/MTU/burst overran the per-datagram or per-frame ceiling — a different MTU, burst, or rate may pass on the same box (e.g. 1344B saturates at 1M but passes at 100–300K).
  • Don’t extrapolate one size to another. Coalescing and per-frame costs are size-dependent.

The core caveat: benchmark result ≠ message cap. A row that says “1M msg/s, P50 42 µs, ✅ 100%” means “at the rate we asked for (1M), with this size/MTU/burst, latency was 42 µs and nothing dropped.” It does not mean “1M is the cap.” The cap is wherever the knee is for your traffic shape — which may be far below 1M (even, large messages) or above it (bursty, tiny messages). Always find the knee for your workload; never quote the headline as capacity.

5b. Setting expectations with the product team

Section titled “5b. Setting expectations with the product team”

This is where the damage usually happens. An engineer’s “we measured sub-100 µs at 1M/s” becomes, three meetings later, a customer-facing SLA of “sub-100 µs at a million messages a second” — and then production misses it by 50× and it’s a credibility crisis. The benchmark number is a lab reading under conditions you chose; the product commitment must be a promise you can keep on the worst production day. They are almost never the same number.

Never hand the product team a raw benchmark figure. Translate it into a sentence with all its conditions, then state the committable version separately:

❌ What NOT to say✅ What to say
”Aeron does 1M msg/s at sub-100 µs.""In a lab best-case (small messages, pre-batched, no business logic), we saw ~56 µs at 1M/s. Our committable target for your traffic shape is P99.9 < X µs at Y msg/s, with headroom to Z."
"P50 is 42 µs.""Median is ~42 µs under load below the knee; the number that matters for your SLA is the P99.9 tail, which is A µs, and the worst-case Max under a burst, which is B."
"It’s not saturated.""It sustains Y msg/s with ρ ≈ 0.6 headroom; beyond ~K msg/s for your message size it saturates and latency goes to milliseconds.”

Commit on the tail, at your real operating point, with headroom

Section titled “Commit on the tail, at your real operating point, with headroom”
  • Quote P99.9 (and Max), never P50. Product and customers feel the tail — a 42 µs median with a 14 ms P99.9 is a 14 ms product, not a 42 µs one.
  • Commit at your provisioned operating point (ρ < ~0.7), not at the knee. The knee is where it breaks; you promise the comfortable rate below it.
  • Commit the burst=1 / your-trace number, not the burst=10 headline (unless you have engineered and shipped the micro-batching that earns it — then commit that system’s measured number).
  • Add explicit margin. Internal target should beat the external SLA by a safety factor, because production has GC, business logic, failover, and noisy neighbors the benchmark didn’t.

Every performance commitment should travel with its five qualifiers, or it’s not a commitment — it’s a rumor: rate, message size, MTU, arrival shape (burst/trace), and percentile. “P99.9 < 200 µs at 300K msg/s, 288 B, MTU 8192, production-trace arrivals” is a promise you can test and keep. “Sub-100 µs” is not.

  1. Lab best-case (the headline): “what the transport can do in ideal conditions” — useful for competitive positioning, never an SLA.
  2. Committable target (your burst=1/trace knee, minus margin): “what we promise for your workload.” This is the SLA input.
  3. Breaking point (the knee / saturation rate): “where it degrades” — so capacity planning and alerting have a real red line.
  • The saturation cliff. Latency isn’t linear — it’s flat, then it falls off a cliff past the knee (56 µs → 5 ms). Tell them there’s a hard edge, where it is, and that you provision below it.
  • The DPDK “60× faster” trap. If someone quotes a huge speedup, explain it’s mostly a saturation artifact and the honest below-knee gap is ~1.3–1.5× — so DPDK buys ceiling and tail stability, not a blanket latency multiplier.

Bottom line for the product team: “Here is what it can do in a lab (best-case), here is what we will commit for your actual traffic (with headroom and on the tail), and here is where it breaks. Please quote the middle one — with its conditions — never the first.”

6. “DPDK is 8× / 60× / 200× faster than the Java driver” — the myth

Section titled “6. “DPDK is 8× / 60× / 200× faster than the Java driver” — the myth”

The kernel-bypass DPDK driver is meaningfully faster and lower-jitter than the Java/C media driver. But the eye-popping ratios are an apples-to-oranges artifact of comparing a healthy DPDK point against a saturated Java point — and you don’t have to take our word for it, because AWS’s own 2025 Aeron benchmark shows exactly this.

From the AWS Industries post “Aeron on AWS: 2025 Performance Benchmark Results” (all on c6in.16xlarge, Cluster Placement Group, MTU 1408):

Aeron Cluster @ 100k msg/s (P99, µs):

DriverP99
OSS – Java136
OSS – C139
Premium (kernel bypass / DPDK)98

→ At 100k, Premium is ~1.4× better at P99. Not 8×. Not 59×. 1.4×.

Aeron Cluster @ 1 million msg/s (P50 / P99 / P99.9, µs):

DriverP50P99P99.9
OSS – Java3,3018,4799,306
OSS – C4,9488,5778,987
Premium (kernel bypass / DPDK)106143158

→ At 1M, OSS has fallen off the knee (P99 8,479µs = 8.5 ms), Premium is still flat (143µs). AWS itself describes this as “a 59× performance improvement.”

Read those two tables together and the whole story is right there: the same two drivers are 1.4× apart at 100k and 59× apart at 1M. The driver didn’t get 40× better between those rows. The OSS driver saturated and the DPDK driver didn’t. The “59×” is not a speed multiplier — it is the distance between a system past its knee and a system still below it. AWS even flags the setup: the 1M figure is meant to reflect “realistic peak and burst rates,” and “production systems are typically over-provisioned to avoid network saturation” — i.e. you would not run OSS at that saturated point in production; you’d provision below the knee, where the gap is small.

The AWS Transport (non-cluster) tables tell the identical tale: @100k, OSS-Java P99 32µs vs Premium 29µs (basically tied); @1M, OSS-Java P99 57µs vs Premium 39µs (~1.5×). The blog’s own summary line — “Premium outperforms OSS by 33% (Transport) and 29% (Cluster)” — is the honest, below-knee multiplier: roughly 1.3×, nowhere near the headline 59×.

Reading rule for any DPDK-vs-standard claim: ask “are both drivers below their knee at this operating point?” If yes, expect a low-single-digit× difference. If the standard driver is saturated and the bypass one isn’t, the huge ratio is a saturation artifact — it tells you where the OSS knee is, not how much faster the transport is. Kernel bypass is also not free (dedicated cores, hugepages, NIC/PCI binding, platform- specific setup), so weigh it against whether you actually need the higher ceiling or the tighter tail — not against an abstract “8× faster.”

  • Do I know the arrival shape (burst, paced vs open-loop) behind this row?
  • Does the message size + MTU match production?
  • Is this the P50 headline or the P99.9 tail — and which does my SLA care about?
  • Is the box below its knee at this rate, or saturating? (“Saturated” / degraded status?)
  • Am I comparing like-for-like (both drivers unsaturated, same size/rate/burst)?
  • Have I found the knee for my traffic, rather than quoting the headline as capacity?
  • Does my spec choice leave ρ < ~0.7 headroom at peak arrival?
  • Have I computed Q_burst by superposing ALL coincident producers (not a per-source rate), and is my socket buffer / receiver window / term buffer ≥ Q_burst × wire-bytes/msg?
  • Am I about to hand this number to a non-engineer? If so, have I translated it to a committable target (tail, at operating point, with conditions) — not the lab headline?

This page is about reading the numbers. For the mechanics of how the driver puts bytes on the wire — TermScanner, flow control, loss recovery — defer to The Aeron Files rather than re-deriving it here. For the buffer sizing that Q_burst feeds into, see receive-path buffers, the buffer sizing calculator, and sender queueing and burst pacing.