AoE2 · LLM Arena

ADR 0002 — Redis Streams as the cross-process broker backend

Status: Accepted (2026-05). Shipped as Phase C. Context: ADR 0001 created the broker abstraction. This ADR picks the cross-process backend.

Decision

Use Redis Streams (XADD / XREAD BLOCK per run) as the second EventBroker implementation, behind ARENA_BROKER_BACKEND=redis. Use INCR for the Seq source and embed it as the left half of the stream ID.

What we considered

OptionProsCons
Redis StreamsAlready in the compose stack (Langfuse needs it). Native XADD MAXLEN matches the in-process deque. BLOCK makes consumers cheap. Single-binary ops.Redis isn’t a “real” message broker — replication semantics aren’t NATS/Kafka grade.
NATS JetStreamPurpose-built for this; better delivery guarantees.New ops surface; another container, another set of credentials.
Kafka / RedpandaIndustry standard.Ridiculous overkill for our load; ops complexity dwarfs the architecture.
Postgres LISTEN/NOTIFYPostgres already in stack.NOTIFY drops messages on disconnect; no replay; payload size limit.

Why Redis Streams

Why we kept Seq from INCR (not native Redis IDs)

Seq is a NewType("Seq", int) starting at 1 and totally ordered. We could have used Redis-native <ms>-<n> IDs and translated at the consumer boundary, but every consumer would then need its own translation table. INCR is one round-trip per publish, embedded into the stream ID as <seq>-0, and the identity stays stable across both impls. Cleaner everywhere.

Subtle correctness items we hit

These are documented in the source where they apply, but worth surfacing for ADR future-readers:

Consequences

Positive

Negative