Loading tutorials…
Loading tutorials…
User identification is the single most-broken part of Mixpanel in DIY installs. Anonymous users never merge with logged-in users, retention curves look like 8% when reality is 40%, and every cohort is wrong. Here's the pattern that actually works.
Who this is forFounders and PMs whose Mixpanel data shows broken funnels — users 'disappearing' between signup and activation, retention numbers that don't match reality, and User Profiles missing for most of your customers. This is the fix.
What you'll need
Step 1
Mixpanel has three identification methods: identify, alias, and reset. Each has a specific purpose. Confusing them is the root cause of 80% of identification bugs.
mixpanel.identify(distinct_id) tells Mixpanel 'all subsequent events belong to this user'. Use it after a user authenticates (login OR signup). The distinct_id should be your auth system's stable user ID.
mixpanel.alias(new_distinct_id) creates a link between an anonymous user and their first authenticated identity. Use it EXACTLY ONCE in a user's lifetime — on the moment of signup (not login). After alias, Mixpanel merges pre-signup anonymous events with the new identified user.
mixpanel.reset() generates a fresh anonymous distinct_id. Use it on logout. Without it, the next user on the same browser inherits the previous user's distinct_id and pollutes their profile.
Underneath: when a user first visits, Mixpanel auto-generates a UUID distinct_id and stores it in localStorage. All anonymous events use this UUID. When you call identify or alias, Mixpanel switches to the authenticated ID and (for alias) creates a server-side merge between the two.
Important 2026 update: Mixpanel's new Identity Merge v3 (default for projects created after 2023) auto-handles the alias step on identify in most cases. But the explicit alias on signup is still the safest pattern, especially if you're on an older project.
Step 2
Your authenticated distinct_id should be your auth system's stable user ID — Clerk user ID, Auth0 sub, Supabase auth uid, your DB user.id. Never use email.
The distinct_id MUST be: (1) stable for the user's entire lifetime, (2) unique across all users, (3) not PII.
Bad choices: email (changes when users update it), username (changes), session token (rotates), full name (not unique).
Good choices: Clerk userId (user_abc123...), Auth0 sub (auth0|abc123), Supabase auth.uid (UUID), your database's primary key for users.
If you're on a custom auth system, use the user's row ID from your users table. Never use email — when a customer changes email, their entire Mixpanel history disappears or splits.
Pass the SAME distinct_id from frontend (mixpanel.identify) AND backend (server-side track calls with distinct_id parameter). Mismatch is the #1 cause of split user profiles.
Step 3
On every successful login (not signup), call mixpanel.identify(user.id). This applies subsequent events to the right user profile.
React example with Clerk: in a `useEffect` that runs when `useUser()` returns an authenticated user, call `mixpanel.identify(user.id)`.
Next.js App Router: in a client component that reads the auth state, run identify in useEffect on mount and on auth state changes.
Plain JS / SPA: in your login success handler, after the auth provider returns the user object: `mixpanel.identify(user.id);`
Server-side rendering: identify cannot run on the server (it's a browser-localStorage call). If you SSR an authenticated page, identify runs on hydration — pass the user ID from server to client.
After calling identify, set People Profile properties so the user appears in Mixpanel with their context: `mixpanel.people.set({ $email: user.email, $name: user.name, plan_tier: user.plan, account_created_date: user.createdAt });`
Step 4
On the moment of new signup completion (not login of an existing account), call mixpanel.alias(new_user_id) BEFORE mixpanel.identify. This merges anonymous events with the new account.
The flow: anonymous user visits → fires events with auto-generated UUID → signs up → call mixpanel.alias(newly_created_user.id) → then mixpanel.identify(newly_created_user.id) → fire signup_completed and onward events.
Mixpanel's server merges the previous anonymous UUID's event history with the new authenticated ID. The user shows ONE continuous profile: anonymous browsing → signup → onboarding.
Code example: after your auth provider creates a new account and returns the user object, in the success handler: `mixpanel.alias(user.id); mixpanel.identify(user.id); mixpanel.track('signup_completed', { ... });`
Identity Merge v3 (post-2023 projects): mixpanel.identify alone will merge IF the new ID hasn't been seen before. Calling alias explicitly is still the safest pattern — it works on both old and new projects.
Critically: ONLY call alias on signup, never on subsequent logins. Distinguish 'signup just happened' from 'user logged in' in your code — most auth providers have an `isFirstSignIn` flag or a webhook for user.created vs user.session.
Step 5
On logout, call mixpanel.reset() to clear the distinct_id and generate a fresh anonymous ID. Without this, the next browser user inherits the previous user.
Logout handler: `mixpanel.reset(); mixpanel.track('logout_completed');` — note: reset BEFORE track so the logout event itself fires under the user's authenticated ID.
Actually scratch that — reset clears the ID, so any event after reset fires under the new anonymous ID. The correct order: `mixpanel.track('logout_completed'); mixpanel.reset();`
Without reset on logout: imagine User A logs out at a coffee shop laptop, User B sits down and starts browsing. All of B's anonymous events fire under A's distinct_id. A's profile is polluted with B's behavior. Cohort analysis breaks.
Same pattern on session expiry: when your auth provider invalidates the session, your auth state handler should call mixpanel.reset() in addition to clearing local auth state.
Mobile apps: reset on explicit logout. Don't reset on app background — the same user is coming back.
Step 6
When firing server-side events, pass the distinct_id explicitly so they merge into the same user profile.
Server-side SDKs don't have implicit user context — you must pass distinct_id on every track call.
Node.js example: `mixpanel.track('subscription_renewed', { distinct_id: user.id, plan_tier: 'pro', price_usd: 49.00 });`
The distinct_id you pass server-side MUST match the one the client uses (from your auth provider). If client uses `user_abc123` and server uses `123` (the raw DB ID), they're two different users in Mixpanel.
Pattern: store the client-facing user ID (Clerk/Auth0/Supabase) on your User table as a column. Server events read from this column when firing tracks.
For events fired from webhooks (Stripe purchase, SendGrid email opened), look up the user from your DB using the customer identifier in the webhook payload, then fire the Mixpanel event with the correct distinct_id.
Step 7
Run a full anonymous → signup → identified → events → logout flow. Verify in Mixpanel that ONE user profile shows ALL the events.
Open Mixpanel → Users (left sidebar). Note the count of users currently visible.
In an incognito browser, visit your site. Fire 2-3 anonymous events (browse pricing, view a product, click a CTA).
Sign up with a fresh test email. Watch your code call alias → identify → track('signup_completed').
In Mixpanel → Users, search for the new user (by email or distinct_id). Open their profile.
The profile should show ALL the events: anonymous pricing-page view, anonymous product view, AND the signup event. They merged correctly.
If you see TWO separate profiles (one anonymous with the pre-signup events, one authenticated with only post-signup events), alias didn't fire correctly. Check the order: alias must run BEFORE identify on first signup.
Now log out, log back in. Fire a new event. Confirm it goes to the SAME profile (not a new one). If a new profile appears, your distinct_id is unstable or login is calling alias instead of identify.
Common mistakes
Calling alias on every login instead of only on signup
What goes wrong: Each login attempt creates an alias call. On older projects this corrupts the identity merge chain. On new projects it's a no-op but signals confused code. Symptom: users have intermittent ghost profiles you can't explain.
How to avoid: Call mixpanel.alias ONLY on the moment of new signup. Distinguish first-signup from subsequent logins using your auth provider's flags (Clerk's `isFirstSignIn`, Auth0's user.created webhook, Supabase's signUp vs signIn methods).
Not calling reset on logout
What goes wrong: Shared devices (coffee shop laptops, family computers, kiosks) accumulate cross-user events on whoever was first. User profiles look like Frankenstein collections of multiple people's behavior. Cohort retention curves are completely wrong because 'returning users' include behavior from other people.
How to avoid: Call mixpanel.reset() in your logout handler AFTER firing any final logout-related event. Also call reset on session-expiry handlers.
Using email as distinct_id
What goes wrong: A user changes their email from old@acme.com to new@acme.com. Their entire pre-change event history disappears from their new profile. Or worse: split profiles, half their data under each. Retention reports show this user as 'churned' on Day 30 because they 'never returned' (they did — under the new email).
How to avoid: Use a stable, immutable user ID — your auth provider's userId (Clerk user_abc, Auth0 sub, Supabase uid) or your database primary key. Map email to the $email People Profile property, not to distinct_id.
Different distinct_id on client vs server
What goes wrong: Client fires events as `user_abc123` (Clerk format). Server fires events as `123` (raw DB ID). Mixpanel sees two separate users. Funnel reports show users 'dropping off' between signup (client) and email verification (server) when they actually completed both. Activation rate looks 50% when reality is 100%.
How to avoid: Standardize on ONE distinct_id format across all surfaces. Store the auth-provider ID on your user table as a column. Every server event reads this column. Audit a few users in Mixpanel to confirm client + server events are on the same profile.
Calling identify without People Profile set
What goes wrong: Users get identified but have empty People Profiles. The Users page shows distinct_ids but no emails, names, or plans. You can't search for users by email. Sales asks 'who is user_abc123?' and nobody can answer without an SQL query.
How to avoid: Immediately after mixpanel.identify, call mixpanel.people.set({ $email, $name, plan_tier, created_at, ... }). Make this part of your identify wrapper so it can't be skipped.
Not handling SSR / hydration timing
What goes wrong: Next.js or another SSR framework renders the page server-side. The first event (page_view) fires before client-side identify runs. That event is anonymous. The user shows two profiles per page load on subsequent visits.
How to avoid: Defer initial event firing until after the auth state has loaded on the client. Use a useEffect that fires page_view only after auth state is known. Or pass user ID from server to client via a hidden context and identify before any tracking calls.
Recap
Done — what's next
How to set up Mixpanel event tracking the right way
Read the next tutorial
Hand it off
Broken user identification silently destroys every cohort, funnel, and retention chart. A vetted product analytics specialist can audit your identify/alias/reset chain across web + mobile + server in 4-6 hours and fix the underlying bugs for $80-160 total at $14-16/hr — and the impact compounds across every report you'll build afterward.
See specialist rates
Mostly no — Identity Merge v3 auto-handles merging when you call identify with an ID Mixpanel hasn't seen before. But calling alias explicitly on signup is still the safest pattern: it works on older projects, it's explicit in code review, and it removes ambiguity in edge cases (multi-tab signup, server-side signup completion). Cost is one extra line of code.
Mixpanel switches the active distinct_id to the new value. All subsequent events fire under the new ID. Identity Merge v3 will attempt to merge the previous anonymous events into the new ID's profile. On older projects without merge, the anonymous events stay under the old UUID.
Each account gets its own distinct_id. When a user logs out of Account A and into Account B (same browser, same person), call reset between sessions so their events route to the right profile. Mixpanel will NOT auto-merge accounts unless you explicitly call mixpanel.identify with $merge_id parameter (Identity Management API).
Yes — anonymous events work with the auto-generated UUID. This is fine for traffic analysis but breaks user-level retention, cohorts, and funnels. For any product-led SaaS, identifying users on signup/login is essential.
distinct_id is Mixpanel's universal identifier — it's anonymous on first visit, then becomes your authenticated user ID after identify. user_id is your application's user ID, which you pass to mixpanel.identify(user_id). After identify is called, distinct_id == user_id. In practice you can treat them as the same thing post-identification.
Mixpanel
Mixpanel doesn't fail because events break — it fails because event names drift. Three engineers, three opinions, three versions of 'signup' over a year. Here's how to ship instrumentation that holds up.
Mixpanel
Cohorts are how Mixpanel goes from 'analytics tool' to 'user-targeting engine'. The team that learns to build, sync, and curate cohorts well runs marketing 2-3x more efficiently than the team that doesn't.
PostHog
PostHog has a one-line install — and a hundred ways to get it wrong. This walks through web, Next.js App Router, React Native, and the server SDK (which you need for any event that can't be lost). With the autocapture gotchas that show up at month two.
Mixpanel
Product analytics is a job, not a tool. The teams that pretend it's a tool spend 18 months building a Mixpanel project that doesn't answer their questions. The teams that hire someone get clean answers in a quarter. Here's how to know which path you're on.