A consumer group is named progress over a stream: leases, retries, dead-letters, and a contiguous ACK frontier — separate from the immutable event log. Many groups can read the same events independently.
Create a group
:ok = Rheo.create_stream("market-events", partition_count: 4)
:ok = Rheo.create_group("market-events", "risk")
:ok = Rheo.create_group("market-events", "surveillance")Optional start cursor (preferred replay path):
:ok = Rheo.create_group("market-events", "risk-replay", start_after: 1_000)Competing consumers (same group)
Workers that share {stream, group} compete for leases. Each event is offered
to one worker at a time (until lease expiry / nack).
{:ok, a} = Rheo.fetch("market-events", "risk", limit: 10, consumer_id: "risk-1")
{:ok, b} = Rheo.fetch("market-events", "risk", limit: 10, consumer_id: "risk-2")
# a and b claim disjoint available workIndependent groups
Different group names progress separately on the same stream:
{:ok, risk} = Rheo.fetch("market-events", "risk", limit: 5)
{:ok, surv} = Rheo.fetch("market-events", "surveillance", limit: 5)
# Same event ids can appear in bothOTP surface
Prefer Rheo.Consumer so a local Rheo.Group owns demand, concurrency, and
lease renewal:
defmodule MyApp.RiskConsumer do
use Rheo.Consumer, stream: "market-events", group: "risk"
@impl true
def handle_event(event, _context) do
case Risk.process(event) do
:ok -> :ack
{:temporary, reason} -> {:retry, reason}
{:permanent, reason} -> {:reject, reason}
end
end
endAlternatively feed Broadway / GenStage with Rheo.Producer — still one group,
different runtime. Do not run both a Consumer and a Producer on the same
{rheo, stream, group} unless you want competing consumers by design.
Partitions and lag
With partition_count > 1, sequences and ordering are per partition. Group
progress is a contiguous frontier; holes block lag advancement:
{:ok, lag} = Rheo.lag("market-events", "risk")
lag.lag
lag.partitions[0].frontierSee Dequeuing and the tutorial ACKs are not a cursor.