Understanding your unique user count is core to product analytics, but PostHog's distinct_id system can be confusing if you're not careful. Get this wrong and you'll either undercount (if users have multiple IDs) or overcount (if you're tracking anonymous + identified separately). Here's how to do it right.
Setting Up Unique User Tracking
PostHog identifies users through a distinct_id. You assign this when initializing the SDK or capturing events. The distinct_id is what PostHog uses to count uniques.
Install and initialize PostHog
Install the PostHog SDK and call PostHog.init() with your API key (find it in Settings > API Keys in your PostHog instance). This sets up event capture for your entire app.
import PostHog from 'posthog-js'
PostHog.init('phc_your-api-key-here', {
api_host: 'https://your-instance.posthog.com'
})Identify users with a stable ID
When a user logs in or their identity becomes known, call PostHog.identify() with a stable identifier (user ID, email, phone—whatever is unique and permanent). This is the distinct_id PostHog uses to count them as one unique user.
PostHog.identify('user-12345', {
email: '[email protected]',
name: 'Alice Johnson',
plan: 'enterprise',
signup_date: '2024-01-15'
})Capture events tied to that user
Once identified, every PostHog.capture() call is attributed to that distinct_id. PostHog counts this as one unique user even if they generate dozens of events.
PostHog.capture('dashboard_viewed', {
dashboard_id: 'dash-456',
time_spent_seconds: 120
})
PostHog.capture('report_exported', {
format: 'pdf',
num_pages: 45
})identify(), PostHog auto-assigns a browser-based distinct_id to anonymous users. Once you call identify(), PostHog merges the anonymous history with the new identified user—this is automatic, no manual action needed.Querying Your Unique User Count
PostHog gives you multiple ways to see unique user numbers: the visual Insights interface for quick charts, or SQL for precise, filtered queries.
Create a Trends insight in the UI
Go to Insights > New Insight, select Trends, and set the metric to Unique users. Choose your date range (last 7 days, month-to-date, etc.) and PostHog graphs your unique count over time.
// Fetch user trends via PostHog Insights API
const response = await fetch(
'https://your-instance.posthog.com/api/projects/{project_id}/insights/trend/',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${POSTHOG_PERSONAL_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
events: [{ id: 'unique_users' }],
date_from: '2024-03-01',
date_to: '2024-03-31'
})
}
)
const data = await response.json()Write a SQL query for custom filtering
Go to Insights > SQL and write raw SQL against PostHog's events table. This lets you filter by event type, user properties, geography, or any other dimension.
// Count unique users in the last 30 days
const query = `
SELECT COUNT(DISTINCT distinct_id) as unique_users
FROM events
WHERE timestamp >= NOW() - INTERVAL 30 DAY
`
// Or execute via API
const response = await fetch(
'https://your-instance.posthog.com/api/projects/{project_id}/query/',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${POSTHOG_PERSONAL_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ query, query_type: 'SQL' })
}
)
const result = await response.json()Filter by specific events or user properties
Narrow your unique count to users who did something specific. Count users who purchased, users in a specific country, or users with a particular plan level.
// Count unique users who completed a purchase in the last 7 days
SELECT COUNT(DISTINCT distinct_id) as paying_users
FROM events
WHERE event = 'purchase'
AND timestamp >= NOW() - INTERVAL 7 DAY
-- Or by user property (country from person object)
SELECT COUNT(DISTINCT e.distinct_id) as us_users
FROM events e
JOIN persons p ON e.person_id = p.id
WHERE p.properties->>'country' = 'US'
AND e.timestamp >= NOW() - INTERVAL 30 DAYCOUNT(DISTINCT distinct_id) and a user has both anonymous and identified activity, they may appear in your data twice until identify() merges them. This is temporary—once merged, they're counted as one.Understanding Anonymous vs. Identified Users
PostHog tracks both anonymous and identified users. You need to understand the difference to avoid double-counting.
Know the difference between distinct_id and person_id
A distinct_id is the identifier you assign (via identify())—your user ID, email, or phone. Before identify(), PostHog assigns a browser-based distinct_id. The person_id is PostHog's internal ID for a person (created after identify() links activity together).
// Count ONLY identified users (those you've called identify() on)
SELECT COUNT(DISTINCT person_id) as identified_users
FROM events
WHERE person_id IS NOT NULL
AND timestamp >= NOW() - INTERVAL 30 DAY
-- Count ALL distinct_ids (including anonymous)
SELECT COUNT(DISTINCT distinct_id) as total_tracked
FROM events
WHERE timestamp >= NOW() - INTERVAL 30 DAYMerge anonymous and identified activity
When you call PostHog.identify(), PostHog automatically merges the anonymous distinct_id with the new identified user. Their event history is linked—no manual action needed. This happens in the background.
// User lands anonymously—PostHog auto-assigns a distinct_id
// They browse for 10 minutes
// They sign up; you now know their ID
PostHog.identify('user-789', {
email: '[email protected]'
})
// From this point on, all events (old and new) are merged under 'user-789'
// PostHog automatically created the link—no extra call neededCommon Pitfalls
- Forgetting to call
identify()on login—anonymous users are counted as separate uniques, inflating your actual user count. - Using inconsistent distinct_ids (sometimes user ID, sometimes email, sometimes a session ID)—this fragments users across multiple IDs and breaks your counts.
- Not filtering out test/bot events—your dev team, QA, and seed data are counted as real users unless you explicitly exclude them in queries.
- Calling
identify()too late—if you identify a user after they trigger events, their pre-identification activity may not merge cleanly depending on your implementation.
Wrapping Up
You now know how PostHog counts uniques: via distinct_id on the front end, merged with person_id for identified users, and queryable via Insights or SQL. The critical move is consistent identification—call identify() early, use the same stable ID throughout the user's lifetime, and filter test data out of your queries. If you want to track this automatically across tools, Product Analyst can help.