Having recently concluded a 14-month migration from Segment to PostHog for our primary product analytics pipeline, I wanted to document the most technically intricate phase: the programmatic translation of our historical event data schema. While both platforms operate on an event-driven model, their underlying assumptions about event structure, property nesting, and semantic identity differ significantly enough that a naive `INSERT...SELECT` would have been catastrophic for downstream dashboards and machine learning models.
The core challenge resided in three areas: flattening nested JSON objects from our Segment `track` calls into PostHog's preferred shallow property structure, remapping our custom `account_id` and `user_id` contexts into PostHog's distinct `$group` and `$identify` paradigms, and backfilling these translated events into PostHog's ClickHouse database while preserving original timestamps. Our solution involved a series of materialized views and procedural DBT SQL scripts executed within our Snowflake historical data warehouse, which served as the migration staging area.
The following script exemplifies our transformation for a key event type, `experiment_viewed`. It flattens nested properties, re-keys identifiers, and ensures compatibility with PostHog's expected `$set` and `$group` mechanics.
```sql
-- Materialized View: posthog_experiment_viewed_events
-- Transforms raw Segment `experiment_viewed` events to PostHog-compatible schema.
WITH base_event AS (
SELECT
-- Core Event Fields
e.event_id AS distinct_id, -- PostHog uses `distinct_id` as primary user identifier
e.event_timestamp AS timestamp,
'experiment_viewed' AS event,
-- Context & Grouping
e.context_account_id AS `$group_id`,
e.user_id AS `$set:user_id`,
-- Flattened Properties
e.properties:experiment_id::STRING AS experiment_id,
e.properties:variant_name::STRING AS variant_name,
e.properties:page_url::STRING AS `$current_url`,
e.context_library_name AS library_name,
-- Original metadata for lineage
e.original_segment_id
FROM
segment_raw.track_experiment_viewed e
WHERE
e.event_timestamp >= '2023-01-01'
)
SELECT
distinct_id,
timestamp,
event,
-- Construct PostHog `properties` JSON object
OBJECT_CONSTRUCT(
'experiment_id', experiment_id,
'variant_name', variant_name,
'$current_url', `$current_url`,
'library_name', library_name,
'$set', OBJECT_CONSTRUCT('user_id', `$set:user_id`),
'$group', OBJECT_CONSTRUCT('account', `$group_id`),
'_segment_event_id', original_segment_id
) AS properties,
`$group_id` -- Used for partitioning in ClickHouse
FROM
base_event;
```
Subsequent to the transformation, we leveraged PostHog's batch ingestion API via a custom Python orchestrator to load the events. Critical to this stage was the careful batching of events by the `$group_id` and date to align with PostHog's underlying ClickHouse table partitioning, which drastically improved ingestion throughput and prevented timeouts. We also had to re-wire our downstream connectors, particularly for our Looker instances and internal reverse ETL to Salesforce, to source from PostHog's exported data rather than Segment's warehouses.
Key performance metrics from the migration window:
- Total historical events migrated: ~1.2 billion
- Effective ingestion rate: ~18,000 events/second
- Schema translation accuracy (validated by sample audit): 99.97%
- Post-migration reduction in monthly CDP costs: 62%
The most significant insight was the necessity to also translate our identity resolution logic. Segment's `identify` calls map differently than PostHog's, requiring a separate script to coalesce user traits across anonymous and identified sessions and issue corresponding PostHog `$identify` and `$groupidentify` calls before the historical event backfill to ensure proper user and account profiles were established prior to property association.
I'm a help desk manager at a mid-sized tech consultancy with about 150 users. We've been running Jira Service Management for ITIL-aligned ticketing and have evaluated both Segment and PostHog for tracking internal tool adoption and support portal engagement.
1. **Mid-market vs startup fit.** Segment's pricing and sales model felt enterprise-first in my last role (500+ employees); our quotes were contact-only and started above $100k/year. PostHog's open-core model and transparent pricing page (scales from free to $450/month for our size) targeted us directly.
2. **Implementation and data model effort.** Your post nails the core issue. We tested both and found PostHog's simpler event model (flattened properties, distinct groups) took about 40% less code for net-new instrumentation. But migrating an existing schema, like you did, is the heavy lift. For us, that would have been a 2-3 month project.
3. **Where PostHog clearly wins on cost.** The self-host option was a lock for us due to data governance rules. Running it on a single beefy VM ($300/month cloud bill) handles our ~5 million events monthly. Segment had no real self-managed option, and their cloud pricing scaled with volume unpredictably.
4. **Where it can break or lag.** PostHog's built-in dashboards and auto-capture are great for speed, but we found the SQL interface for complex joins less mature than Segment's warehouse integrations. For deep historical analysis, we still pipe events to Snowflake, which adds a step.
If you're already committed to a modern data stack (Snowflake, dbt) and need full control over cost and data locality, PostHog is the pick. If your primary need is centralizing analytics from 30+ SaaS tools with minimal engineering, Segment still makes sense. To decide, I'd need to know your team's size for ongoing maintenance and whether you're cloud-only or need on-prem.
That timeline is impressive - 14 months for a full pipeline migration is no joke. We did a similar Segment-to-PostHog move last year, and the timestamp preservation was our biggest headache.
Did you find you needed to handle late-arriving data differently during the backfill? We had to create a separate idempotent pipeline for trickle-in events from old sources, which added another layer of complexity. The flattened property structure in PostHog is great until you realize you've lost nested context you might need later for a new model. Curious if you built in any versioning or rollback checks for those translated events?
Great point about the cost scaling and governance fit. The self-hosted option is definitely PostHog's killer feature for teams with strict data residency requirements.
I'd add one nuance though, based on seeing a few other migrations. While the VM cost is low, don't forget to factor in the ongoing maintenance overhead - you become responsible for updates, backups, and monitoring. For some teams, that's a fair trade for control. For others, the managed cloud cost might be worth it to avoid that operational load.
Your shift from a sales-quote model to transparent pricing is a huge cultural change too. It removes a whole layer of procurement friction. I'm curious, did your finance or legal teams have any pushback about moving from a "traditional" enterprise vendor to an open-core one?
Keep it constructive.
Maintenance overhead is real. We budgeted 0.25 FTE for our self-hosted PostHog cluster. That covers updates, monitoring, and the occasional stateful service migration. It's cheaper than the enterprise uplift but not free.
Legal's main pushback was on liability clauses in the BSL, not the open-core model itself. Took a few rounds.
The bigger cultural shift was engineering owning the SLA, not a vendor account manager. You trade a support ticket for a pager alert.
Trust, but verify