Deep Dive

The Aeron + SBE transport

Reliable UDP and IPC messaging under a zero-copy binary wire protocol, and why it beats JSON-over-HTTP for latency-sensitive clients.

Why it matters

The fastest cache lookup in the world is worthless if the request spends a millisecond being parsed. For latency-sensitive clients — market data, pricing, risk — the transport is the latency budget. Aeron Cache offers a native path that removes almost everything JSON-over-HTTP spends its time on: text parsing, header bloat, connection setup, and allocation. That path is SBE over Aeron, and it is the same class of machinery used to move messages inside trading systems.

What Aeron is

Aeron (version 1.50.0 in gradle/libs.versions.toml) is a reliable, high-throughput messaging transport. It provides ordered, reliable delivery over UDP unicast/multicast or, when publisher and subscriber share a machine, IPC via shared-memory log buffers — bypassing the network stack entirely. Messages are written into pre-allocated log buffers; a media driver handles flow control, retransmission, and the reliability UDP itself doesn’t give you. The same fabric carries the RAFT replicated log (see RAFT consensus); the client gateway reuses it to carry commands and streaming updates.

Aeron Cache exposes this through its Aeron gateway: bidirectional commands plus streaming subscriptions over Aeron, on UDP :7075 / :7076 or over IPC. It is opt-in — AERON_GATEWAY_ENABLED defaults to false — and the medium is chosen with GATEWAY_TRANSPORT_MEDIA = udp | ipc. Co-locate a client with the node and ipc gives you shared-memory messaging with no kernel network path at all.

The SBE wire protocol

Simple Binary Encoding (SBE) is a FIX-standard codec for low-latency binary messages. Instead of serializing objects into text, SBE generates flyweight codecs that read and write fields directly against a buffer at fixed offsets — no intermediate objects, no reflection, no allocation. The sbe-tool (1.33.0) compiles a schema into Java/Rust codecs at build time.

Aeron Cache’s gateway schema lives at cache-aeron-gateway/aeron-gateway-server/src/main/resources/sbe/gateway-schema.xml. Its header tells you the ground rules:

<sbe:messageSchema xmlns:sbe="http://fixprotocol.io/2016/sbe"
                   package="com.bhf.aeroncache.gateway.messages"
                   id="7"
                   version="0"
                   byteOrder="littleEndian">
  <types>
    <composite name="messageHeader">
      <type name="blockLength" primitiveType="uint16"/>
      <type name="templateId"  primitiveType="uint16"/>
      <type name="schemaId"    primitiveType="uint16"/>
      <type name="version"     primitiveType="uint16"/>
    </composite>
    <composite name="varStringEncoding">
      <type name="length"  primitiveType="uint32" maxValue="1073741824"/>
      <type name="varData" primitiveType="uint8" length="0" characterEncoding="UTF-8"/>
    </composite>
  </types>

Three facts worth internalizing: schema id 7, little-endian byte order (matching the dominant CPU architectures, so no byte-swapping), and varStringEncoding — a length-prefixed UTF-8 run for every variable-length string (cache ids, keys, values, correlation ids). Fixed fields sit at known offsets; variable fields follow, each self-describing its length. A decoder walks the buffer once.

The command and streaming messages are thin. A single GatewayCommand carries the entire request/response command surface, discriminated by msgType:

<sbe:message name="GatewayCommand" id="1">
    <field name="msgType"       id="1" type="uint16"/>
    <field name="ttl"           id="2" type="int64"/>
    <field name="counterValue"  id="3" type="int64"/>
    <data  name="correlationId" id="4" type="varStringEncoding"/>
    <data  name="cacheId"       id="5" type="varStringEncoding"/>
    <data  name="key"           id="6" type="varStringEncoding"/>
    <data  name="value"         id="7" type="varStringEncoding"/>
</sbe:message>

<sbe:message name="GatewayStreamUpdate" id="11">
    <field name="eventType"     id="1" type="UpdateEventType"/>
    <data  name="cacheId"       id="2" type="varStringEncoding"/>
    <data  name="key"           id="3" type="varStringEncoding"/>
    <data  name="value"         id="4" type="varStringEncoding"/>
    <data  name="correlationId" id="5" type="varStringEncoding"/>
</sbe:message>

Every correlationId is echoed on the matching response, so a client multiplexes many in-flight requests over one connection and matches replies without ordering assumptions. The enums in the schema — OperationStatus, UpdateEventType, SubscriptionMode, BulkOperationType, TimerType — are single uint8 values that mirror the cluster’s own model exactly, so a status like UNKNOWN_CACHE or an event like PATCH_ITEM crosses the wire as one byte.

Why binary-over-Aeron beats JSON-over-HTTP

Argue it from architecture, not a benchmark:

JSON over HTTP                         SBE over Aeron
--------------------------------       --------------------------------
TCP connect + TLS handshake            persistent Aeron publication
HTTP/1.1 request+response framing      one framed message, templateId
parse JSON text -> object graph        read fields at fixed offsets
allocate strings/maps per request      zero-copy flyweight, no alloc
header + whitespace overhead           uint8 enums, length-prefixed data
one request per round trip             many correlated msgs, one channel
kernel network stack                   UDP, or IPC shared memory

HTTP pays for human-readability on every message: text tokenizing, object graphs that churn the garbage collector, and per-request connection and header overhead. SBE pays none of it — fields are read in place, enums are bytes, and the codec allocates nothing on the hot path (see low-GC design). Over IPC the kernel network stack disappears too. The predictability matters as much as the raw speed: no parser means no parser-induced jitter, which is what tail-latency-sensitive workloads actually care about.

The catch: Java and Rust only

The trade-off is reach. The SBE codecs are generated for Java and Rust, so the Aeron gateway transport is available to those two client languages. The embedded clients confirm the split: HTTP+WebSocket and bidirectional WebSocket work across all four languages (Java, TypeScript, Python, Rust); the Aeron SBE gateway is Java and Rust only. If you need the native path from TypeScript or Python, the bidirectional WebSocket (/api/ws/v1/bidi) gives you the same correlated command surface over a more portable transport (see embedded clients and streaming subscriptions).

Takeaway: HTTP is the universal front door; SBE-over-Aeron is the service entrance for clients that measure latency in microseconds and can run Java or Rust. Same cluster, same semantics, radically different wire.