Event ingestion filtering: keep only the first event per person per day, gated by a feature flag
@jakesciotto is already working on this.
Since Sep 4, 2026.
- Dominant language
- Python
- Stars
- 39.9k
- Forks
- 3.4k
- Avg merge
- 6h 51m
- Merged PRs (30d)
- 232
Description
Feature request
Problem
A customer runs a distributed architecture. Many nodes send one keepalive event for the same person, dozens of times per person per day. That event is the majority of their event volume. They use the first event per person per day for analysis.
- Their nodes share no state. No node knows if another node already sent the event today.
- No customer-facing tool at ingestion time holds state.
What already exists
nodejs/src/ingestion/common/feature-flag-called-dedup/ dedupes $feature_flag_called per person against Redis. Phil Haack shipped it in #62793.
It has disabled / shadow / drop modes, a team allowlist, an excludedTeams escape hatch, a TTL guard, a JSON-hashed claim key, a local-first check, and Redis metrics.
What is missing
| Gap | Today | Needed |
|---|---|---|
| Key identity | (team_id, distinct_id, flag_key, response, groups, has_experiment) |
(team_id, event_name, distinct_id, day) |
| Day scope | TTL only | The day inside the key |
| Configuration | Environment variable | The event filter tree |
| Rollout control | None | A customer-owned feature flag |
Why the other tools do not work
| Tool | Reason |
|---|---|
| Event ingestion filtering | event_name and distinct_id only, equals and contains only, no state |
| Drop events transformation | No state. Runs after filters. |
| Downsampling (#93939) | Fixed percentage. It keeps the wrong events. |
| Capture overflow limiter | Reroutes above a threshold. It does not drop. The window is 1 minute. |
A workaround works today
Four code paths dedupe on the same identity, (day, event, distinct_id, uuid):
| Path | Expression |
|---|---|
get_teams_with_billable_event_count_in_period |
distinct toDate(timestamp), event, cityHash64(distinct_id), cityHash64(uuid) |
get_teams_with_billable_enhanced_persons_event_count_in_period |
The same expression |
analyticsRecordId in nodejs/src/ingestion/common/steps/usage-records-steps.ts |
day plus sha256([event, distinctId, eventUuid]) |
The events ReplacingMergeTree |
ORDER BY (team_id, toDate(timestamp), event, cityHash64(distinct_id), cityHash64(uuid)) |
So the customer derives the UUID from the person and the UTC day. Every node computes the same UUID, and no node needs shared state. posthog-node accepts uuid on the capture message. prepare-event-step.ts passes it through unchanged.
posthog.capture({ distinctId, event: 'keepalive', uuid: uuidv5(`${distinctId}:${utcDate}`, NS) })
Questions
- Do we support that identity, or must a customer not rely on it? If we support it, then one doc page closes this issue.
get_teams_with_event_count_with_groups_in_periodusescount(1), not the distinct expression. The group analytics add-on therefore bills every duplicate copy. Is that intended?- The
eventsengine version column is_timestamp. The surviving row is the last copy of the day, not the first.
Proposal, if the answer to question 1 is no
Add a seen_today node to the filter tree in nodejs/src/ingestion/common/event-filters/. Call the existing dedup service with a second key builder. Gate the rule on a customer-owned feature flag, which gives the customer a rollout ramp and a kill switch.
- Key: hash a JSON-encoded
(event_name, distinct_id, day)tuple. Do not join the values with a colon. Both fields accept any character, soa:bwithcandawithb:ccollide and drop a first event. - Day: the UTC date of the event timestamp, which matches
toDate(timestamp). - Cost:
unique persons × filtered eventskeys per day. One million persons on one event is about 100 MB. - Failure: a Redis error makes the node evaluate false, and PostHog ingests the event. Fail open.
- Verify: total event count falls, and unique persons per day stays flat.
- Dropped events do not count toward billing.
nodejs/src/ingestion/** has no CODEOWNERS entry, so a PR there requests no reviewer. #91119 and #93845 touch the same filter step, model and API.
Debug info
- PostHog Cloud
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.