R7.04.3 · dive 3
One bitmap per day.
The most common bitmap pattern is one bitmap per day. Set a player's bit when they act, and the
day's activity is a BITCOUNT; roll several days up with BITOP for weekly and monthly
actives, and retention.
This is also where the encoding's one limitation bites hardest, because real ids are rarely a
dense 0..N sequence. The dive ends on the sparse-offset rule, and on the forward-tense
codemojex spike that solves it with a roaring bitmap. Select a window; the readout runs the
BITOP OR over the seven daily rows below.
BITOP OR over the days in it.BITOP OR over a window is "active any day in
it".A bitmap per day — DAU
bitmap-patterns.md.txt · SETBIT dau:<date>Name the bitmap by date; set a player's bit on activity. The day's count is a BITCOUNT:
SETBIT dau:2024-01-30 12345 1 # player 12345 was active on this day BITCOUNT dau:2024-01-30 # daily active users (DAU) GETBIT dau:2024-01-30 12345 # was this player active that day?
One bitmap per day, one bit per player. The day's DAU is BITCOUNT over its key. Memory: 1
million players = 125 KB per day — a year of daily-active bitmaps for a million players is about
46 MB, far less than a Set of ids per day.
A bitmap named per day makes DAU a BITCOUNT, at 125 KB per million players per day.
BITOP over days — WAU, MAU, and retention
bitmap-patterns.md.txt · union and intersectionWindows are BITOP over the daily bitmaps:
# weekly active — set in ANY day this week BITOP OR wau:2024-W05 dau:2024-01-28 dau:2024-01-29 dau:2024-01-30 ... BITCOUNT wau:2024-W05 # active EVERY day this week — the most-engaged BITOP AND wau:2024-W05:every dau:2024-01-28 dau:2024-01-29 ... BITCOUNT wau:2024-W05:every
BITOP OR over a week's daily bitmaps is weekly-active; over a month's, monthly-active.
BITOP AND over them is "active every day in the window". The result is itself a bitmap, so it
composes — a quarter is the OR of its months. Retention is the intersection
of two days — select the cohort and watch the BITOP AND:
day0 AND dayN.# of those active on day 0, who returned on day 7 BITOP AND retain:d0_d7 dau:2024-01-01 dau:2024-01-08 BITCOUNT retain:d0_d7 # day-7 retention rate = BITCOUNT(retain:d0_d7) / BITCOUNT(dau:2024-01-01)
A funnel is a chain of BITOP AND: registered-and-played-and-paid is the intersection of three
attribute bitmaps. Every cohort question reduces to a union or an intersection followed by a count.
Windows and retention are BITOP over daily bitmaps: OR for "active any
day", AND for "active every day" or "active on day 0 and day N". The result composes into the next
question.
The sparse-offset limitation
bitmap-patterns.md.txt · the highest-offset ruleThe density holds only over a dense id space, because a bitmap allocates memory up to its highest set offset:
SETBIT users:active 999999999 1 # allocates ~125 MB to hold ONE bit
One bit at offset ~10&sup9; forces the String to ~125 MB of mostly zeros. The source's fixes are: use dense,
sequential ids; hash ids to a bounded range and accept collisions; or use a Set for sparse
data. A plain bitmap is the right structure only when the ids are packed near 0. This is the fact
that decides the codemojex design below.
The codemojex cohort note — a retired spike, and roaring
branded_id.ex · infra/cm-bitmapist (retired)codemojex ships no bitmap code today: there is zero SETBIT/BITCOUNT/bitmapist
in echo/apps/codemojex/lib/. What follows describes a retired spike that never shipped.
An analytics spike, cm-bitmapist (infra/cm-bitmapist/ until 2026-10-11 — a Go port of
Doist's bitmapist4, branded-id-native, on its own Fly machine), would model daily-active
players as bitmaps. The offset would be each player's branded-id placement — the
hash32, the first half of MurmurHash3's fmix64 truncated to 32 bits, asserted at boot in
branded_id.ex:
-- the placement (hash32) is the bit offset a branded-native bitmap would use
placement("USR0KHTOWnGLuC") → 234878118
A page-own codemojex example uses the live brand PLR (player):
# retired spike — cm-bitmapist, never in echo/apps/codemojex Mark("active", "PLR0KHTOWnGLuC", today) # offset = placement("PLR0KHTOWnGLuC") Count("active", month) # BITCOUNT — monthly active players AndCount(registered, played, paid) # a registered → played → paid funnel
Here is where the sparse-offset rule decides the design. The branded placement is a hash, so
the offset lands anywhere in the 32-bit space — sparse, exactly the §3 limitation. A flat bitmap over
a sparse branded keyspace is enormous; the spike used roaring bitmaps instead (compressed
bitmaps that store dense runs and sparse positions differently, not a flat allocation to the highest offset).
The published result for a sparse branded keyspace is on the order of ~129 GB on a plain bitmap versus
~300 MB roaring — the §3 limitation, solved by the structure. The hash32 is also
collision-bearing in 32 bits (on the order of N²/2³³ colliding pairs at N players —
negligible into the millions), so distinct counts undercount slightly; a property to state, not assume.
A bitmap per day; BITCOUNT = DAU; BITOP OR/AND over days = WAU/MAU and retention
— exact cohort analytics over the bits.
codemojex's retired cm-bitmapist spike would have marked daily-active players
at the offset = their branded-id placement (placement(…) → 234878118). The placement
is sparse, so the spike used a roaring bitmap, not a flat one.
A bitmap per day makes DAU/WAU/MAU and retention a matter of BITCOUNT and
BITOP — exact, cheap, composable. But only over a dense id space: a hashed (branded) id space is
sparse, so a branded-native cohort store wants a roaring bitmap, the structure that does not allocate to the
highest offset.
Notes on ValkeyBITOP OR|AND|XOR|NOT destkey key [key ...]
writes a result bitmap the size of the longest input and is O(N) in that length; a flat String bitmap grows to
its highest set offset, which is why a sparse id space wants a different structure —
valkey.io/commands/bitop.
The branded-id placement vector
The 14-character branded id's placement is the 32-bit offset the retired cohort spike would have marked.
Where the id vectors live and are asserted at boot is the Branded Component System
overview — placement("USR0KHTOWnGLuC") → 234878118.
Back to the module
The three dives ground in the same standalone bitmap pattern — the flag, the aggregates, the daily rollup. Return to the Bitmaps hub, or on to the next module in the chapter.
References
sources · relatedSources
- Valkey — BITOP — union and intersection of daily bitmaps for windows and retention.
- Valkey — BITCOUNT — DAU/WAU/MAU as a population count.
- Valkey — SETBIT — set a player's daily bit; the offset is the id, and a flat bitmap allocates to the highest offset.
- Redis — Bitmaps — the bitmap data-type overview.
Related in this course
- R7.04 · Bitmapsthe module hub
- R7.04.1 · 1-bit flags
SETBIT/GETBIT, the offset is the id - R7.04.2 · bitcount aggregates
BITCOUNTandBITOPanalytics - /bcs · Overviewthe branded-id placement vector