Realtime Cohort Calculation (MVP)
@gustavohstrassburger is already working on this.
Since Nov 3, 2025.
- Dominant language
- Python
- Stars
- 39.9k
- Forks
- 3.4k
- Avg merge
- 7h 27m
- Merged PRs (30d)
- 222
Description
MVP
The MVP is complete when we can calculate and query cohort membership for behavioral cohorts such as:
person performed event X at least 5 times in the last 30 days AND
person has a posthog.com email address AND
(person’s first name is Max OR Maxine) AND
person is in Cohort ABC
or:
person has not performed event X in the last 30 days AND
person belongs to Cohort B (a realtime cohort)
Goal:
Feature Flags and Workflows teams can query the cohort_membership table directly for membership status.
Additional Requirements
-
Handle person merges (currently via ClickHouse query)
-
Handle duplicate events (currently via ClickHouse logic)
-
Implement backfilling (no solution yet)
-
Migrate existing cohorts that can be behavioral
-
Support “has ever performed X” feature
-
Support “has performed X for the first time” feature
-
Backfilling!
Architecture (Flow)
┌───────────────────────────┐
│ Django App │
│ - Saves Cohorts │
│ - Generates Bytecode & │
│ conditionHash │
└───────────────────────────┘
┌───────────────────────────────────────────────┐
│ Kafka: events_clickhouse_json │
│ (Raw events from ingestion pipeline) │
└────────────┬──────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────┐
│ Event Ingestion Service (Node) │
│ - Loads cohorts for team │
│ - Evaluates events via hogvm │
│ - Matches against cohort bytecode │
│ - Publishes matches to Kafka │
└────────────┬──────────────────────────────────┘
│ matched events
▼
┌───────────────────────────────────────────────┐
│ Kafka: pre-calculated-events topic │
└────────────┬──────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────┐
│ ClickHouse │
│ - Stores pre-calculated-events │
│ - Queried by Temporal for behavioral checks │
└───────────────────────────────────────────────┘
┌───────────────────────────────────────────────┐
│ Temporal Cohort Workflow (Periodic Trigger) │
│ - Initiates cohort recalculation │
│ - Queries: │
│ • Behavioral (ClickHouse) │
│ • Person │
│ • inCohort/notInCohort │
│ - Publishes membership changes │
└────────────┬──────────────────────────────────┘
│ membership changes
▼
┌───────────────────────────────────────────────┐
│ Kafka: cohort-membership-changed │
└────────────┬──────────────────────────────────┘
│
┌────────────┴──────────────────────────────────┐
│ Cohort Membership Consumer │
│ - Writes to Postgres (lookup) │
│ - Publishes to Kafka (trigger topic) │
└────────────┬──────────────────────────────────┘
│
┌───────┴────────────────────┬───────────────────────────┐
│ │ │
▼ ▼ ▼
┌────────────────────────────┐ ┌────────────────────────────┐
│ Postgres: cohort_membership │ │ Kafka: cohort-membership- │
│ - Queried by Flags, │ │ changed-trigger │
│ Workflows, APIs │ │ (for dependent systems) │
└────────────────────────────┘ └────────────────────────────┘
Bytecode Generation for Cohorts (Django)
Events
To determine whether an event should be ingested into the pre-calculated-events table (for later cohort membership calculations), cohorts must generate bytecode that can be evaluated in real time using hogvm.
Purpose
- The bytecode serves as an event matcher, e.g. “has performed event X”.
- The temporal aspect (e.g. “at least 3 times in the last 30 days”) is handled later when querying the
pre-calculated-eventstable. - Queries use a
conditionHashderived from the bytecode.
Handling Negations
- Negated filters (e.g. “has not performed event X”) generate the same bytecode as their non-negated counterpart.
- This ensures events are ingested for absence checks (e.g. “has not performed event X in the last 30 days”).
Cohort Dependencies
- The
is(not)InCohortcondition is supported only for realtime cohorts. - A cohort is marked as
realtimewhen all its filters produce valid bytecode that can be evaluated by hogvm.
Persons
The pre-calculated-events table is used only for event matching, enabling queries like “has performed event X in the last 30 days.”
Person Property Conditions
Behavioral cohorts can include person property conditions, such as:
person.property.email contains "@posthog.com"
These person-based conditions are evaluated during cohort membership calculation within temporal workflows that query the person table.
Potential Optimization
If this approach is too slow, we can apply the same pre-calculation pattern used for events.
To prepare for that, we already:
- Generate bytecode for person-property evaluations.
- Compute a
conditionHashto allow potential lookup in a futurepre-calculated-person-propertiestable.
Current State
Currently, the person-property bytecode is not actively used — it exists for future optimization.
Missing Work
- Generate bytecode for behavioral filters used by
pre-calculated-events-consumer@meikelmosby (PR) - Create unified
conditionHashalgorithm for writers and temporal worker @meikelmosby (PR) - Re-save all cohorts to generate
byte_codeandcohort_type@meikelmosby- Write management command for testing @meikelmosby (PR)
- migrate team2 to use realtime cohorts @meikelmosby
- migrate rest of posthog over @meikelmosby
- Write migration for production/hobby deployments @meikelmosby
- Ensure
inCohortis implemented and working properly @meikelmosby
Event Ingestion Service (Behavioral Event Consumer in Node)
This service consumes events from the events_clickhouse_json topic and loads all cohorts for a given team to evaluate each event against the cohort’s bytecode.
Event Matching and Publishing
- For each cohort, the service checks if the event matches the cohort’s bytecode condition.
- If a match is found, the service publishes an event to a topic that populates the
pre-calculated-eventstable in ClickHouse.
Key Implementation Details
De-duplication of Filters
All filter conditions across cohorts are iterated and deduplicated to avoid duplicate entries, resulting in a unique set of condition hashes per team.
Event-to-Condition Mapping
Each event is matched against all unique filter conditions, writing one entry to the table per matching condition hash.
Exclusion of Person-Property Filters
Person property filters are filtered out — they are not handled by this service.
Missing Work
- use cohorts instead of actions, handle deduplication & person-property condition filtering @meikelmosby (PR)
Cohort Membership Calculation (Temporal)
Cohort membership is calculated using Temporal workflows, which evaluate three main components:
- Behavioral conditions – e.g. “performed event X at least N times over the last M days”
- Person property conditions – e.g. “email contains @posthog.com”
- Cohort membership conditions – e.g. “is in Cohort A” or “is not in Cohort B”
Behavioral Conditions
- Loads all
realtime(behavioral) cohorts for a team. - Extracts filter conditions (JSON) and associated
conditionHash. - Constructs a ClickHouse query to evaluate membership against:
pre-calculated-eventscohort-membership(ClickHouse)
- Determines whether a person has joined or left a cohort based on event history.
Person Property Conditions
- Person property filters (e.g.
email contains "@posthog.com") are currently evaluated via Postgres queries. - If slow, a pre-calculated table similar to
pre-calculated-eventscan be introduced.
Cohort Membership Conditions
- Handles dependencies like
isInCohortandisNotInCohort. - Evaluates via the
cohort-membershiptable in Postgres. - Only realtime cohorts are eligible for real-time referencing.
Final Evaluation and Publishing
- Combines results from all conditions to determine final membership.
- Publishes changes (join/leave) to the
cohort-membership-changedKafka topic.
Missing Work
- calculate cohort membership based on cohorts and not actions @gustavohstrassburger (with help of @meikelmosby ) (PR)
- calculate temporal conditions @gustavohstrassburger (with help of @meikelmosby )
- calculate person conditions @gustavohstrassburger (with help of @meikelmosby )
- calculate
inCohortconditions @gustavohstrassburger (with help of @meikelmosby )
- adjust logic to publish
cohort_membership_changedentry to kafka (make sure we set partition key correctly) @gustavohstrassburger (with help of @meikelmosby ) (PR) - rename cdp-behavioral-events to cdp-precalculated-filters @gustavohstrassburger (PR)
Improvements to handle Team 2:
- add max_execution_time and retries @gustavohstrassburger (PR and PR)
- build
precalculated_person_propertiesand use it inHogQLRealtimeCohortQuery@gustavohstrassburger (PR) - optimize query by merging sibling property groups @gustavohstrassburger (PR)
- use
person_idfromprecalculated_eventsandprecalculated_person_propertiesinstead of joining with persons @gustavohstrassburger (PR, PR, PR) (reviews pending to merge) - "update" precalculated tables when persons are merged @gustavohstrassburger
Cohort Membership Consumer
The consumer reads cohort_membership_changes from Kafka and writes them to Postgres for fast lookups (used by Flags, Workflows, etc.). The service also writes to a kafka topic about the cohort-membership change which can be used to power workflows, etc.
Missing Work
- make postgres fully usable @meikelmosby
- pgbouncer usage
- ensure migrations work (build on top of this PR)
- publish cohort-membership-change result to kafka @meikelmosby PR
- deploy service (charts) @meikelmosby
Misc
ClickHouse
- Create migrations for
cohort_membershiptable, materialized views, and Kafka tables @Daesgar - Move behavioral cohort computations to dedicated Kubernetes nodes @Daesgar
- optimise query perf for cohort membership calculation @Daesgar
Productionization
- use
cohort_membershipto update the field count of the tableposthog_cohort - use
cohort_membershipto match feature flags when using real-time cohorts in conditions - use
cohort_membershipto display persons in the cohort details page - disable former cohort count for realtime cohorts
- Extend Grafana dashboard with all systems and ClickHouse @meikelmosby @Daesgar
- Implement E2E tests
- Write documentation
- Alerts (!!!!) @meikelmosby
Cohort Model Modifications
- Add
backfillproperty to indicate when recalculation is needed - Add flag for “behavioral” (dynamic-v2) cohorts used by Flags and Workflows
- Track staleness state for updates
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.