Blog Product analytics and replay
Cohort retention: how the numbers are counted
A retention chart looks simple: rows of people who started, columns of how many came back. But "came back on day 1" can mean the next calendar day or 24 hours later, and "started" can mean first ever or first this month. Tools make different choices, so the same data gives different numbers.
This post explains exactly what ZipLogger counts, with a run of the real counting code, and then how saved cohorts fit in.
Open a retention report
Go to Events and click Retention in the view switcher. There are three inputs:
- Cohort event: the event that puts someone in a cohort, for example
signup. - Return event: the event that counts as coming back. If you leave it empty, any event counts.
- weekly or daily.
Click Compute. Each row is a cohort, labelled with the day or week it started, then Users, then one column per period: W0 to W7 for weekly, D0 to D13 for daily. Each cell is a percentage of that row's users. Hover a cell for the raw count, for example "1 of 2".
Below the table it says how many events were examined, and how many were skipped for having no user id or anonymous id.
The definition, step by step
This is what the code does (RetentionGrid in the event store, with the same rules in the ClickHouse query):
- A person is the
userId. If an event has nouserId, theanonymousIdis used instead. The two are not merged here. Someone who browsed anonymously and then signed up can count as two people. - Entry is a person's first cohort event inside the report's time window. It is not their first cohort event ever.
- The cohort row is the UTC calendar day of that entry, or for weekly reports the week that starts on Monday, UTC.
- A return is a return event that comes strictly after the entry. With
signupas both cohort and return event, the signup itself does not count as coming back. - The period of a return is how many calendar days or weeks separate its bucket from the entry's bucket.
D1means "did something on the next UTC calendar day". - Each column is counted separately. A person can count in
W1andW3but not inW2. A cell does not mean "still around by then".
Periods that have not finished yet never show a final percentage, so this week's cohort does not look like everyone left. An open period with nobody back shows a dot (·). One where some people are already back shows the count so far with a star, such as 3*. A row whose day or week is only partly inside the window is flagged, because it holds fewer people than a full one.
A run on real code
We fed a small stream through the same RetentionGrid class the event store uses: daily buckets, signup as the cohort event, any event as a return, four periods.
| Who | Events (UTC) |
|---|---|
| a | signup Mon 21 Sep 10:00, page_view Mon 15:00, Tue 09:00, Thu 12:00 |
| b | signup Mon 21 Sep 23:30, page_view Tue 00:15 |
| c | signup Tue 22 Sep 11:00 |
anonymous visitor (anon_7f3) | signup Tue 22 Sep 12:00, page_view Wed 08:00 |
| no ids at all | page_view Wed 09:00 |
The output:
RETENTION daily, cohortEvent=signup, returnEvent=any, periods=4
2026-09-21 users=2 returned=[1,2,0,1]
2026-09-22 users=2 returned=[0,1,0,0]
2026-09-23 users=0 returned=[0,0,0,0]
2026-09-24 users=0 returned=[0,0,0,0]
ignoredWithoutIdentity=1
So the Sep 21 row reads 50%, 100%, 0%, 50%. Notice:
- b counts toward D1 after 45 minutes. They signed up at 23:30 and came back at 00:15, which is the next UTC calendar day. Buckets are calendar days, not rolling 24-hour windows.
- a counts in D0 because they came back the same day after signing up.
- The anonymous visitor is a person here. Retention falls back to the anonymous id, so device-level retention works before anyone signs in. Funnels do not do this.
- Empty days still get a row. Sep 23 and 24 had no signups, and they show up as rows with zero users instead of disappearing. A chart with missing rows would look better than it is.
- The event with no ids was skipped and reported, not dropped without a word.
Calendar buckets and what they mean for you
Because buckets are UTC calendar days and Monday-start UTC weeks, a few things follow:
- Someone who signs up late in the day gets a very short
D0and an easyD1. Someone who signs up just after midnight gets the opposite. W0covers the rest of the entry week, which is anything from seven days to a few minutes.- There is no time zone setting.
If you compare these numbers with a tool that uses rolling 24-hour periods, expect D1 to differ.
The time window
The report picks its own window from the granularity. The UI does not use the page's time range picker. Weekly covers the last 8 weeks, with 8 periods. Daily covers the last 14 days, with 14 periods. The window starts at "now minus 8 weeks", which is usually mid-week, so the oldest row can hold only part of a week's signups.
The API (GET /api/v1/events/retention) takes cohortEvent, returnEvent, granularity, periods (up to 12 weeks or 30 days), and from and to. The UI has no control for the number of periods.
Because entry means "first cohort event in the window", the choice of cohort event matters:
- A one-time event like
signupgives you new-user cohorts. - A repeated event like
page_viewgives you "first seen in this window". A user who has been around for a year enters on their first page view in the window, as if they were new.
The same goes for the return event. Left empty, anything counts, including background events and the $identify event that identify() records. Pick an event that means real use, such as report_viewed.
Saved cohorts
A cohort is a saved audience: everyone who did an event in the last N days, optionally narrowed by property conditions. Click Cohorts… on the Events toolbar. The form takes:
- a name, for example
Recent buyers; - the event, for example
purchase; - last N days, from 1 to 90;
- optional conditions in
name:op:valueform, separated by spaces:plan:eq:business amount:gt:100. The operators areeq,neq,contains,startsWith,endsWith,gt,gte,ltandlte.
Click Create. Each cohort's row has a count button that shows how many users it matches right now. A + after the number means the cohort has more than 10,000 members and counting stopped there. On OpenSearch a query limited to such a cohort is refused, and a process report refuses an audience that large on any store. A workspace can have up to 50 cohorts. Creating and deleting them needs the Editor role.
A cohort stores its definition, not a list of people. Membership is worked out again on every query, so it never goes stale. It also means "purchased in the last 30 days" is always counted back from now, not from the dates of the report you apply it to.
A condition that does not parse, for example plan=business without the :op: part, is dropped without an error, and the cohort then matches more people than you meant. Check the count after you create one.
What you can do with a cohort
Pick a cohort in the Events toolbar, where it says All users by default. The Overview, Explore and Retention views all narrow to its members. With Retention, that answers questions like "do business-plan buyers come back more than everyone else?" Compute once with All users, once with the cohort, and compare the rows.
Two things change when a cohort is applied:
- Only identified users count. Cohort members are user ids, so the anonymous-id fallback no longer applies.
- The cohort filters, it does not define entry. The report still enters people on the cohort event you typed into the Retention form. The saved cohort only decides which users are looked at.
Over the API, ?cohort=<id> works on the event search, histogram, overview, terms, funnel, retention, property values and CSV export endpoints. The dashboard funnel widget has no cohort picker. A process report's Audience setting is how a cohort reaches a funnel in the UI.
Limits
- A scan limit. Retention reads every matching event in the window. Past the server's limit you get the real count and a suggestion: "Narrow the time range, set a return event, or add a filter." Setting a return event helps most, because then only two event names are read.
- No identity merging in the counts. An identity link from
identify()does not join an anonymous person to their account here. - No rolling periods and no time zones. UTC calendar buckets only.
For where people drop out on the way to signing up, see the funnel analysis post. The events API and SDK calls are in the product analytics docs. Or create a free workspace, send a signup event, and open Events → Retention.