← Back to Self-host Mixpanel

Migrate from Mixpanel to PostHog

Product analytics · full migration guide

Mixpanel → PostHog is a SDK swap plus an optional historical backfill. PostHog ships a Mixpanel data import pipeline (Pipelines → Sources → Mixpanel) that pulls historical events via Mixpanel's Raw Data Export API; the live cutover is a few lines of code in your client. The friction is funnel and cohort definitions: those are stored in Mixpanel's UI and have to be rebuilt as PostHog Insights. Plan two weeks: week 1 dual-instrument (run both SDKs side-by-side), week 2 backfill history and rebuild dashboards, then cut Mixpanel.

Prerequisites

  • A self-hosted PostHog instance — see /mixpanel/ for the 30-minute docker-compose recipe and ~$20-40/mo VPS sizing (ClickHouse needs 4GB+ RAM).
  • Mixpanel project admin (you need access to project settings and the Service Account creation).
  • A Mixpanel Service Account with read access to the project (Project Settings → Service Accounts → Create — pick the Project Owner role for the migration window, downscope after).
  • Your project's region (US or EU) and Mixpanel project ID (Settings → Project Settings).
  • A staging window of ~1 week where you can run both SDKs in parallel.

Step 1 — Export from Mixpanel

  1. Find your Mixpanel project ID and region

    In Mixpanel: Settings (top right) → Project Settings → Overview. The Project ID is a numeric value. Region is either `us` (data.mixpanel.com) or `eu` (data-eu.mixpanel.com).

  2. Create a Service Account

    Settings → Project Settings → Service Accounts → + Create Service Account. Name: `posthog-import`. Role: Owner (you can downscope to Consumer post-migration). Mixpanel returns a username and a secret — save both, you only see the secret once.

  3. Verify Raw Data Export API access

    Test with curl: `curl --user '<service-username>:<service-secret>' 'https://data.mixpanel.com/api/2.0/export?from_date=2024-01-01&to_date=2024-01-02&project_id=<project-id>'`. You should get a streaming JSONL response of events. If you get 402, your Mixpanel plan does not include Raw Data Export — only paid Growth+ plans do. In that case, the import is forward-only (no historical backfill).

  4. Decide your backfill window

    Raw Data Export is metered: you pay per million events queried (Growth plans include some). For most teams, 90 days of history is enough — funnels and retention need it, but year-over-year analysis can stay in Mixpanel as a read-only reference. Note the from_date / to_date you'll use for the import.

Step 2 — Import into PostHog

  1. Install PostHog SDKs in your application alongside Mixpanel

    Both SDKs can run in parallel. For JS: `posthog-js` initialized with `posthog.init('<api-key>', { api_host: 'https://your-posthog-host' })` next to your existing `mixpanel.init`. Mirror your `track` calls: every `mixpanel.track('Sign Up', props)` gets a sibling `posthog.capture('Sign Up', props)`. Same event names, same property keys — that's what makes the historical backfill align.

  2. Verify live events are flowing

    PostHog → Activity → Events. You should see events landing within seconds of an action. Verify the same event appears in Mixpanel — the two should be 1:1 modulo a small clock skew. If counts diverge, check for client-side `track` calls that aren't double-instrumented yet.

  3. Run the Mixpanel historical import

    PostHog admin → Data Pipeline → Sources → Mixpanel. Paste service account username, secret, project ID, region. Choose start date (your backfill window). Save. The import runs as a background job — for 10M historical events, expect 1-3 hours. PostHog deduplicates on `$insert_id` (which Mixpanel sets), so re-running is safe.

  4. Map distinct_id → person ID

    PostHog uses `distinct_id` for the same purpose as Mixpanel; the import preserves it. If you set Mixpanel's `$user_id` via `mixpanel.identify()`, PostHog's identify (`posthog.identify(userId)`) does the equivalent. Verify on a known user: PostHog → Persons → search by email. The user's event history should span the full backfill window.

  5. Rebuild funnels, cohorts, and dashboards

    PostHog Insights → New Insight. Recreate each Mixpanel funnel as a PostHog Funnel (Steps → add events). Recreate each Mixpanel cohort as a PostHog Cohort (People → New Cohort → matching properties). Bookmark the Insights into a Dashboard that mirrors your Mixpanel dashboard. Plan an afternoon for a team's worth of insights.

  6. Cut over and remove the Mixpanel SDK

    Once dashboards mirror Mixpanel for 1-2 weeks and the team trusts the numbers, remove the `mixpanel.track` calls. Cancel the Mixpanel project (or downgrade to a free tier as a read-only archive).

Field / concept mapping

Mixpanel PostHog Notes
Mixpanel project PostHog project 1:1. One Mixpanel project becomes one PostHog project.
Mixpanel event PostHog event Same name. Custom property keys preserve. System properties (`$browser`, `$os`) are equivalent in both.
Mixpanel distinct_id PostHog distinct_id Direct mapping — both use the same field name.
Mixpanel $user_id (identified) PostHog user_id via identify() Call `posthog.identify(userId, userProperties)` — same shape as `mixpanel.identify(userId)` followed by `mixpanel.people.set()`.
Mixpanel User Profile PostHog Person Imported from the Mixpanel data export. All `$set` properties become Person properties.
Mixpanel Funnel PostHog Funnel Insight Manual rebuild. Steps and conversion windows transfer conceptually.
Mixpanel Cohort PostHog Cohort Manual rebuild. Property-based filters translate; behavioral cohorts (`did event X 3 times in 7 days`) translate via PostHog's behavioral cohort builder.
Mixpanel Retention PostHog Retention Insight Manual rebuild. PostHog's retention picker is similar.
Mixpanel Group Analytics PostHog Groups Both support multi-entity tracking. Group identify calls translate: `mixpanel.set_group('company', companyId)` → `posthog.group('company', companyId)`.
Mixpanel Annotations PostHog Annotations Manual recreation — both let you mark a date with a release / event note. Re-paste the important ones.
Mixpanel Dashboard PostHog Dashboard Manual rebuild — group your recreated Insights.

Downtime estimate

Zero downtime. The dual-instrumentation approach means Mixpanel keeps running until you trust PostHog. Hard cutover with no overlap is possible (swap `mixpanel.track` for `posthog.capture` in one deploy) but loses the verification window — not recommended unless this is a fresh project with no analytics-driven decisions yet.

Common gotchas

  • Mixpanel's Raw Data Export costs money on usage-based plans. A 1-billion event project can cost low-three-figures USD to backfill. If price-sensitive, do a forward-only migration (skip the historical pipeline).
  • PostHog's open-source self-hosted edition does NOT include some cloud-only modules (data warehouse sync, advanced bulk export). For pure product analytics + session replay + flags, OSS is fine.
  • ClickHouse single-node has a soft scaling ceiling around 5-10M events/day. If you're at Mixpanel-scale (100M+ events/day) you're committing to a real ops project — consider PostHog Cloud instead.
  • Mixpanel's `$insert_id` deduplication is preserved on import — re-running the historical pipeline is idempotent. But if you've already sent the same event live via the SDK during dual-instrument, you may briefly double-count until ClickHouse merges. Funnels stabilize after a day.
  • Property names with dollar signs (`$browser`) work in both, but custom dollar-prefixed properties may collide with PostHog's internal namespace. Avoid `$your_custom_prop`.

Rollback plan

Mixpanel is unaffected during the migration — your project still has all the data. If PostHog doesn't fit, removing the `posthog.capture` calls and pointing dashboards back to Mixpanel is a one-line revert in your client. Keep the PostHog instance running for 30 days as a fallback view; ClickHouse storage is cheap and the data is yours regardless of where queries run.

Looking for setup time, monthly cost, and other alternatives? See Self-host Mixpanel.