How configuration works
Aeron Cache is configured with environment variables. Everything else is a delivery mechanism for them:
| You run it with | Where you set options | Good for |
|---|---|---|
| Homebrew (all-in-one) | ~/.aeron-cache/backend.env | Local desktop use and trying things out |
| Kubernetes / Helm | values.yaml per chart, rendered into a ConfigMap | Clusters, staging and production |
| From source | The .env files in the repo’s config/ directory | Development |
The same option has the same name everywhere. cacheMode in a Helm chart becomes CACHE_MODE in the pod, and is the CACHE_MODE line in backend.env. So once you know an option from one place, you know it in all of them.
If you have not installed anything yet, start with Getting started.
Pick a deployment mode
The biggest decision is which shape you deploy. It sets CACHE_MODE and decides which other options matter.
| Mode | CACHE_MODE | Nodes | Replicated | Use it for |
|---|---|---|---|---|
| All-in-one | RAFT | 1, everything in one process | Single-node RAFT | Laptops, demos, local development |
| Cluster | RAFT | 3 (StatefulSet), plus separate interface apps | Yes, via RAFT | Production, high availability |
| Ephemeral | SINGLE | 1 | No, no consensus | Pure caching where losing state on restart is fine |
All three expose the same command surface and the same APIs, so moving from one to another does not change your client code.
Homebrew (all-in-one)
brew tap bhf/aeroncache
brew install aeron-cache
aeron-cache
On first run a default config is written to ~/.aeron-cache/backend.env. Edit it and restart aeron-cache. Lines are plain NAME=value.
Choosing interfaces
The REST API and cluster tools are always on. The streaming gateways are switchable:
| Variable | Default | What it controls |
|---|---|---|
WEBSOCKET_GATEWAY_ENABLED | true | WebSocket gateway (uni- and bidirectional streaming) |
SSE_GATEWAY_ENABLED | true | Server-Sent Events gateway |
AERON_GATEWAY_ENABLED | false | Native Aeron SBE gateway |
GATEWAY_TRANSPORT_MEDIA | udp | udp or ipc. Only read when the Aeron gateway is enabled |
A disabled gateway is not started at all, so the matching UI streaming features will not connect.
For example, to enable the Aeron gateway over IPC and turn SSE off:
AERON_GATEWAY_ENABLED=true
GATEWAY_TRANSPORT_MEDIA=ipc
SSE_GATEWAY_ENABLED=false
Near cache
An optional read-ahead cache inside the HTTP, WebSocket (including the bidirectional socket), SSE and Aeron gateway interfaces. When on, the interface subscribes to the cluster, keeps a warm local copy of the caches it touches, and serves reads from it. It is transparent to clients (same URIs, no client change); see the API page for what is served from it and the management endpoints.
| Variable | Default | What it controls |
|---|---|---|
NEAR_CACHE_ENABLED | false | true serves HTTP reads and WebSocket/SSE/bidi/Aeron-gateway hydration snapshots from the near cache. Read by each interface, so set it for the ones you want |
Because it is read-ahead, a read of a warm key can briefly lag a concurrent write. Leave it off if you need strongly consistent reads.
Other all-in-one options
| Variable | Default | What it controls |
|---|---|---|
CACHE_MODE | RAFT | RAFT (replicated) or SINGLE (ephemeral) |
DYNAMIC_CACHE_CREATION | false | Whether caches can be created on the fly |
MONOLITH_EMBEDDED_DRIVER | true | true starts an embedded Aeron media driver. false attaches to an external aeronmd already running at AERON_DIR |
JVM options and heap size
To pass JVM flags such as a larger heap, set JAVA_TOOL_OPTIONS in backend.env:
JAVA_TOOL_OPTIONS=-Xmx4g
Several flags go in one value, separated by spaces, for example JAVA_TOOL_OPTIONS=-Xms4g -Xmx4g. Restart aeron-cache after editing.
- Why this works: the launcher exports every line of
backend.envas an environment variable before starting Java, and the JVM readsJAVA_TOOL_OPTIONSby itself. - Not
JAVA_OPTS: the launcher sets that variable itself, so a value inbackend.envis overwritten. - Not command-line arguments:
aeron-cache <args>passes them to the application, not to the JVM. - Default: with no flag, the JVM sizes the heap at a quarter of physical RAM.
- Check it applied: the JVM logs
Picked up JAVA_TOOL_OPTIONS: ...at the top of~/.aeron-cache/aeron-cache.log. - Shell variable: a
JAVA_TOOL_OPTIONSin your shell is replaced by the value inbackend.env. - Native memory: Aeron’s term buffers live outside the heap, so a larger
-Xmxdoes not change them. See term length.
Core options (all deployments)
These are shared by the cluster, interface apps and all-in-one install.
| Variable | Helm value | Default | What it controls |
|---|---|---|---|
CACHE_MODE | cacheMode | RAFT (SINGLE for ephemeral) | Replicated RAFT cluster, or single unreplicated node |
DYNAMIC_CACHE_CREATION | n/a | false | Allow caches to be created dynamically |
CLUSTER_ADDRESSES | clusterAddresses | the three StatefulSet pod DNS names | Comma-separated hosts of the cluster nodes |
CORS_ALLOWED_ORIGINS | corsAllowedOrigins | localhost ports 3000 to 3005 | Browser origins allowed to call the API, for example the UI |
AERON_CACHE_TERM_LENGTH | aeronCacheTermLength | 128m in the charts (application default 64k) | Aeron term buffer length for cache data channels. See below |
IDLE_STRATEGY | idleStrategy | backoff | How agent and Aeron threads wait when idle. See below |
Term length and maximum value size
The maximum Aeron message size is termLength / 8. The charts set 128m so values up to roughly 16 MB work. Leave it blank to fall back to the application default of 64k, which suits small values only. The buffers are sparse, so the resident memory cost of a larger term length is small.
Idle strategy: latency versus CPU
The idle strategy decides what a thread does when it has no work. It is the main latency-versus-CPU dial.
| Value | Behaviour | Trade-off |
|---|---|---|
backoff (default) | Spins, then yields, then parks | Balanced. Good default |
spin | Busy-spins | Lowest latency, burns a full core per thread |
yield | Yields the CPU | Lower CPU than spin, slightly more jitter |
noop | Never waits | Burns CPU. Testing only |
sleep-ns[:nanos] | Sleeps for a period, for example sleep-ns:1000 | Low CPU, higher latency |
sleep-ms[:millis] | Sleeps for milliseconds, for example sleep-ms:1 | Lowest CPU, highest latency |
| a class name | A fully qualified IdleStrategy class with a no-arg constructor | Custom |
IDLE_STRATEGY applies to every role. To tune one role, set IDLE_STRATEGY_<ROLE>, which takes priority over the global value. For example the HTTP app reads IDLE_STRATEGY_BLOCKING_PUBLISHER, IDLE_STRATEGY_CLUSTER_CLIENT_AGENT, IDLE_STRATEGY_CLUSTER_MESSAGE_PUBLISHER, IDLE_STRATEGY_AGENT_RUNNER and IDLE_STRATEGY_UNCLUSTERED. An invalid value fails fast at startup rather than when the first agent is created.
Snapshots and log purging
A RAFT cluster appends every command to a log. Snapshots let a node restart quickly, and purging discards log history older than the retained snapshots. The on-demand snapshot-and-purge endpoint always works. The options below control the scheduled run.
| Variable | Helm value (snapshot.*) | Default | What it controls |
|---|---|---|---|
SNAPSHOT_SCHEDULE_ENABLED | scheduleEnabled | false | Turn the scheduled snapshot and purge on |
SNAPSHOT_INTERVAL | interval | PT6H | Period for the ALWAYS strategy (ISO-8601 duration) |
SNAPSHOT_RETENTION_COUNT | retentionCount | 2 | Recent snapshots whose log history is kept |
SNAPSHOT_CHECK_STRATEGY | check.strategy | ALWAYS | Which check gates each scheduled run. See below |
SNAPSHOT_CHECK_POLL_INTERVAL | check.pollInterval | PT5M | How often the two conditional strategies are evaluated |
SNAPSHOT_CHECK_STATE_CHANGE_THRESHOLD | check.stateChangeThreshold | 10000 | State-changing operations needed before snapshotting |
SNAPSHOT_CHECK_ARCHIVE_GROWTH_BYTES | check.archiveGrowthBytes | 268435456 (256 MiB) | Archive growth needed before snapshotting |
Choose the check strategy by what you want to avoid:
SNAPSHOT_CHECK_STRATEGY | Snapshots when | Pick it when |
|---|---|---|
ALWAYS | Every SNAPSHOT_INTERVAL | You want a simple, predictable schedule |
CACHE_STATE_CHANGE | Enough state-changing operations have happened since the last snapshot | Traffic is bursty and idle periods should not snapshot |
RECORDING_GROWTH | The archive has grown enough since the last purge | You are bounding disk use |
Scheduling only applies to clustered (RAFT) runs where CLUSTER_FOLDER is set. In SINGLE mode the options are inert. Durations use ISO-8601 form, for example PT30M for 30 minutes and PT6H for 6 hours.
Kubernetes and Helm
The charts live in k8s/helm/ in the main repository. Each component is its own chart, and the Makefile there wires them together.
git clone https://github.com/bhf/aeron-cache
cd aeron-cache/
make all # build images into Minikube
cd k8s/helm/
make install-all # cluster + interfaces + UI
Which chart do I need?
| Chart | What it deploys | Default replicas |
|---|---|---|
aeroncache-cluster | The RAFT cluster nodes, with a clustertools sidecar | 3 |
aeroncache-ephemeral | A single, unreplicated node (CACHE_MODE=SINGLE) | 1 |
aeroncache-monolith | Cluster node plus all interfaces in one pod | 1 |
aeroncache-http | REST interface (port 7070) | 1 |
aeroncache-ws | WebSocket interface | 1 |
aeroncache-sse | SSE interface | 1 |
aeroncache-gateway | Native Aeron SBE gateway (ports 7075 and 7076) | 1 |
aeroncache-http-clustertools | Cluster tools over HTTP | 1 |
aeroncache-ui-nextjs | Web UI | 1 |
aeroncache-observability | Prometheus and Grafana | n/a |
Near cache in the charts
Near caching is a switch on the interface charts, not a separate chart (the old aeroncache-http-near chart and image have been removed).
| Chart | Value | Default |
|---|---|---|
aeroncache-http | nearCacheEnabled | true |
aeroncache-ws | nearCacheEnabled | false |
aeroncache-sse | nearCacheEnabled | false |
aeroncache-gateway | nearCacheEnabled | false |
Each sets NEAR_CACHE_ENABLED on the container. For example: helm upgrade --install aeroncache-ws ./aeroncache-ws --set nearCacheEnabled=true. Each replica holds its own copy of the caches it has warmed, so it costs memory per replica.
Install targets
All targets run from k8s/helm/, and every install-* has a matching uninstall-*.
make install-backend # cluster + HTTP, WS, SSE, gateway
make install-frontend # UI only
make install-all # backend + UI
make install-backend-ephemeral # single-node backend
make install-all-ephemeral # single-node backend + UI
make install-monolith-all # all-in-one: node + gateways + UI
make uninstall-all # tear down
Individual components work too, for example make install-http or make install-ui.
Make flags
Flags go on the end of any install target.
| Flag | Effect |
|---|---|
EXTERNAL_MEDIA_DRIVER=true | Run the Aeron C media driver (aeronmd) as a sidecar instead of embedding the Java driver |
GHCR=true | Pull released images from ghcr.io instead of using images built into Minikube |
GHCR_TAG=1.2.3 | With GHCR=true, pin a specific release |
JAVAAGENT=true | Enable the OpenTelemetry Java agent on the interface apps so they serve Prometheus metrics |
make install-all GHCR=true GHCR_TAG=1.2.3
make install-backend-ephemeral GHCR=true EXTERNAL_MEDIA_DRIVER=true
Common values
These appear in most charts. Defaults below are from the chart values.yaml files.
| Value | Default | What it controls |
|---|---|---|
replicaCount | 3 for cluster, 1 otherwise | Pod count. Keep the cluster at an odd number for RAFT quorum |
cacheMode | RAFT (SINGLE for ephemeral) | See core options |
idleStrategy | backoff | See idle strategy |
aeronCacheTermLength | "128m" | See term length |
image.repository, image.tag, image.pullPolicy | local image, latest, Never | Which image to run. Never suits Minikube. Use IfNotPresent or Always with a registry |
service.type | ClusterIP (cluster), LoadBalancer (interfaces, all-in-one) | How the service is exposed |
ingress.enabled | false | Create an Ingress, with className, annotations, hosts and tls |
resources | {} | CPU and memory requests and limits. Unset by default, so set them for production |
autoscaling.enabled | false | Horizontal pod autoscaling on interface apps, with min/max replicas and a CPU target |
nodeSelector, tolerations, affinity | empty | Standard pod placement |
Cluster chart specifics
| Value | Default | What it controls |
|---|---|---|
persistence.enabled | true | Persist cluster state on a volume. Sets CACHE_DATA_DIR |
persistence.size | 1Gi | Volume size |
persistence.storageClass | "" | Storage class. Empty uses the cluster default |
persistence.mountPath | /data | Where the volume is mounted |
service.clusterIP | None | Headless service, so pods get stable DNS names |
service.publishNotReadyAddresses | true | Required. RAFT nodes must resolve their peers before they are Ready, so without this the nodes crash-loop at bootstrap |
clustertools.port | 7080 | Port for the sidecar serving liveness, readiness and Prometheus metrics |
clustertools.snapshot.* | see snapshots | Scheduled snapshot and purge |
The cluster needs /dev/shm for Aeron’s shared memory. The chart mounts a 1 GiB in-memory emptyDir there by default. Raise sizeLimit if you raise term lengths or run many streams.
All-in-one chart specifics
| Value | Default | What it controls |
|---|---|---|
websocketEnabled | true | WebSocket gateway |
sseEnabled | true | SSE gateway |
aeronGatewayEnabled | false | Aeron SBE gateway |
gatewayTransportMedia | "udp" | udp or ipc |
ports.http / ws / sse / clusterTools | 7070 / 7071 / 7072 / 7080 | Listener ports |
corsAllowedOrigins | localhost ports | Allowed browser origins |
External media driver
By default each node embeds a Java media driver. Setting externalMediaDriver.enabled=true runs the C aeronmd driver as a sidecar and attaches the node to it as a pure client (LAUNCH_EMBEDDED=false).
| Value | Default | What it controls |
|---|---|---|
externalMediaDriver.enabled | false | Use the C media driver sidecar |
externalMediaDriver.aeronDir | /dev/shm/aeron | Aeron directory shared by driver and node over the shm volume |
externalMediaDriver.threadingMode | SHARED | SHARED, SHARED_NETWORK or DEDICATED |
externalMediaDriver.extraEnv | [] | Extra AERON_* variables for the driver, for example driver timeout or idle strategies |
externalMediaDriver.resources | {} | Resources for the driver container |
Build the driver image with make build-media-driver, which is part of make build-backend.
Observability
Deploy Prometheus and Grafana with make install-observability. It installs into the aeroncache namespace and ships a pre-provisioned “Aeron Cache - Interface Apps” dashboard covering JVM memory, CPU, GC and HTTP request rate and latency.
There are two sources of metrics:
- Clustertools sidecar. Cluster and ephemeral pods carry
prometheus.io/*annotations, and the sidecar serves the shared media driver counters on/prometheusat port7080. This works with no extra flag. - OpenTelemetry Java agent. Optional and off by default. Pass
JAVAAGENT=trueto an interface-app install target, or setjavaagent.enabled=true, and the app serves metrics onjavaagent.metricsPort(default9464) atjavaagent.metricsPath(default/metrics). Traces and logs exporters default tonone.
Sending telemetry to an external collector (OTLP)
The agent is the standard OpenTelemetry Java agent, so it reads the standard OTEL_* environment variables. The chart sets a few of them for you and passes anything in javaagent.extraEnv straight through, which is how you point it at your own collector.
| Value | Default | What it controls |
|---|---|---|
javaagent.serviceName | chart fullname | Sets OTEL_SERVICE_NAME |
javaagent.tracesExporter | none | Sets OTEL_TRACES_EXPORTER. Use otlp to ship traces |
javaagent.logsExporter | none | Sets OTEL_LOGS_EXPORTER. Use otlp to ship logs |
javaagent.extraEnv | [] | Extra environment variables, such as the endpoint and protocol |
To ship traces and logs to a collector while keeping the Prometheus metrics endpoint:
# values override for an interface app, e.g. aeroncache-http
javaagent:
enabled: true
serviceName: aeroncache-http
tracesExporter: otlp
logsExporter: otlp
extraEnv:
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://otel-collector.observability.svc.cluster.local:4318
- name: OTEL_EXPORTER_OTLP_PROTOCOL
value: http/protobuf
Use port 4317 with grpc instead if your collector expects gRPC. Per-signal variables such as OTEL_EXPORTER_OTLP_TRACES_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS (for authenticated backends) work the same way. If the endpoint is unreachable, the agent logs export failures, so leave tracesExporter and logsExporter at none until the collector is up.
The metrics exporter is fixed to prometheus by the chart. To push metrics over OTLP as well, add OTEL_METRICS_EXPORTER to extraEnv with the value otlp or prometheus,otlp. It is appended after the chart’s own entry, so it takes precedence, but check the rendered pod spec with helm template before relying on it.
This applies to the interface apps only. The clustertools sidecar serves Prometheus metrics on port 7080 and is not configured through these values. Scrape it, or have your collector scrape it with a Prometheus receiver.
Overriding values
Use standard Helm. Either edit the chart’s environments/values.dev.yaml, or pass overrides:
helm upgrade --install aeroncache-cluster ./aeroncache-cluster \
--namespace aeroncache \
--set idleStrategy=backoff \
--set persistence.size=10Gi \
--set clustertools.snapshot.scheduleEnabled=true \
--set clustertools.snapshot.check.strategy=RECORDING_GROWTH
Recipes
Local, minimal
Install with Homebrew and leave everything at its defaults. You get REST, WebSocket and SSE, plus the UI.
Local, native Aeron access
# ~/.aeron-cache/backend.env
AERON_GATEWAY_ENABLED=true
GATEWAY_TRANSPORT_MEDIA=ipc
IPC works when the client runs on the same machine. Use udp when it does not.
Cluster with bounded disk
# values override for aeroncache-cluster
persistence:
size: 20Gi
clustertools:
snapshot:
scheduleEnabled: true
retentionCount: 2
check:
strategy: RECORDING_GROWTH
archiveGrowthBytes: 536870912 # 512 MiB
Cluster tuned for latency
idleStrategy: "spin" # a full core per thread, so give the pods dedicated CPU
externalMediaDriver:
enabled: true
threadingMode: DEDICATED
resources:
requests: { cpu: "4", memory: 4Gi }
limits: { cpu: "4", memory: 4Gi }
Measure before and after. Spinning trades CPU for latency, and the right answer depends on your hardware and load. See Low-GC design for the design reasoning.
Large values
Keep aeronCacheTermLength: "128m" for values up to about 16 MB. Leaving it blank drops back to 64k, which is far smaller.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Cluster pods crash-loop at startup | service.publishNotReadyAddresses was turned off. Nodes cannot resolve their peers |
| UI shows no streaming | WEBSOCKET_GATEWAY_ENABLED or SSE_GATEWAY_ENABLED is false |
| Browser calls fail with a CORS error | The UI’s origin is not in CORS_ALLOWED_ORIGINS |
| Brew install runs out of heap | Set JAVA_TOOL_OPTIONS=-Xmx... in backend.env. JAVA_OPTS is ignored |
| Large values are rejected | AERON_CACHE_TERM_LENGTH is too small. Max message size is an eighth of it |
| Scheduled snapshots never run | SNAPSHOT_SCHEDULE_ENABLED is false, the mode is SINGLE, or CLUSTER_FOLDER is unset |
| Pod will not start with an external driver | The shm volume is missing or aeronDir differs between driver and node |
A GET returns a slightly stale value after a write | NEAR_CACHE_ENABLED=true: warm reads are eventually consistent. Unset it for strongly consistent reads |
| Pod fails on image pull | image.pullPolicy is Never but the image is not in the cluster. Build it, or use GHCR=true |
Where the source of truth lives
Defaults can change between releases. The main repository is authoritative: the config/*.env files for environment variables, and k8s/helm/<chart>/values.yaml for every Helm option, each annotated with comments.