Architecture

How qbrix works

qbrix separates the hot path — serving a decision — from the learning path that makes the next one better. Neither one waits on the other.

Two paths that never block each other.

A decision reads cached parameters and returns. Learning happens behind a queue. The two meet in one place — the parameter cache — and nowhere else.

01Two paths
Hot pathsynchronous · no training on the request
Your appselect() with context
proxysvcauth, gates, routing
motorsvcstateless selection
Rediscached parameters
Learning pathasynchronous · never on the request
Your appfeedback() with reward
proxysvcpublishes the event
Redis Streamsfeedback · selection · audit
cortexsvcbatch training

The only shared surface is the parameter cache. cortexsvc writes updated parameters; motorsvc reads them on a TTL. If training stops, decisions keep being served from the last good parameters — they simply stop improving.

A request, end to end.

Two calls, two completely different journeys. One returns on your request; the other is allowed to take as long as it needs.

02A request
select()Returns on your request
  1. 1Your app calls select() with an experiment id and whatever context it has.
  2. 2proxysvc authenticates the key, resolves your tenant from that credential, and checks the feature gate.
  3. 3The request is routed to a motorsvc replica over gRPC.
  4. 4motorsvc reads the experiment's parameters from its in-memory TTL cache, falling back to Redis on a miss.
  5. 5The policy samples a variant and returns it with a selection id.
  6. 6proxysvc publishes a selection event and returns the variant. Your render continues.
feedback()Returns immediately, learns later
  1. 1Your app calls feedback() with the selection id and a reward.
  2. 2proxysvc publishes a feedback event to the stream and returns straight away — nothing waits on training.
  3. 3cortexsvc reads a batch from the stream under its own consumer group.
  4. 4The policy is trained on that batch and new parameters are computed.
  5. 5Parameters are written to Redis. The stream entry is acknowledged only once training has completed.
  6. 6The next selection whose cache entry expires picks up the new parameters.

Where state lives.

Four stores, and only one of them is read to make a decision.

03State
StoreHoldsOn the hot path
PostgresTenants, users, API keys, pools, variants, experiments, feature gates, subscriptions and invoices.Not for the decision itself — the request authenticates against it.
RedisPolicy parameters and experiment configuration, read through an in-memory TTL cache.Yes — the only thing a selection reads.
Redis Streamsfeedback, selection and audit events, with per-consumer-group offsets.No — the durable hand-off between the two paths.
ClickHouseEvery event, append-only, for analytics and the event log.No — analytics only, and Enterprise Edition.

What happens when something breaks.

The honest version. Most of these degrade the quality of decisions rather than stopping them, which is the whole reason the paths are separate.

04Failure
Training stops

Decisions keep being served from the last good parameters. They stop improving, but nothing returns an error and nothing falls back to random.

A selection replica dies

It holds no durable state, so it is replaced. There is no drain and no migration of your data.

A consumer crashes mid-batch

Stream entries are acknowledged only after the work is durably done, so the batch is redelivered on restart rather than lost.

A pod is rolled

SIGTERM is handled: in-flight work drains within a bounded grace budget before the process exits. Buffered events are flushed rather than dropped.

One event cannot be decoded

That single entry is quarantined, logged with its payload and counted — it does not stall the consumer behind it forever.

The parameter cache is unavailable

Selection degrades rather than stopping: replicas already serving an experiment keep answering from the parameters they hold, and pick up fresh ones when the cache returns. This is the dependency the hot path genuinely has, which is why it is the one we engineer hardest around.