/

Data Integration

Best Way to Sync PostHog to Postgres (2026)

Intergalactic Data Labs

โ€”

โ€”

11 min read

Table of contents

Summarize

PostHog's own batch export is the only supported way to land raw events in Postgres, and a connector tool carries the entities around them. Eight paths work as of October 2026, split by whether they move events, entities, or both, and by whether you pay per row, per active row, or run the reader yourself.

Path

Best for

Main limit

Filament

Persons, cohorts, flags, experiments, and surveys as typed tables

No events, persons re-read in full each run

PostHog batch export

Raw events, persons, and sessions on a schedule

Per-row billing past the free million, no flags or cohorts

Fivetran PostHog Lite

One managed connector for events and entities

Active-row billing, PostHog Cloud only

Airbyte

Self-hosted events plus persons and flags

Events stream reads the deprecated endpoint

Estuary

Events and sessions refreshed every five minutes

Reads events through the query endpoint PostHog restricts

ingestr

One-command CLI copy of persons, flags, and events

FSL license, persons paged in full

Sling

Scripted pulls inside a wider Sling setup

API connections need a CLI Pro token

Plain script

Anything the project API exposes

You own paging, retries, and rate limits

The split exists because PostHog deprecated its events API and tells third-party connectors to use batch exports, while the persons endpoint has no modified-since filter. Of Fivetran, Airbyte, and similar tools, PostHog's docs say "we do not recommend using those to move data from PostHog to other destinations" (PostHog, ETL tools).

The five facts that decide this sync

Five facts about PostHog's data model and API decide which path fits.

  • Events and entities are different shapes. Events are immutable, high volume, and stored in ClickHouse, "our main analytics backend" in PostHog's words (PostHog, how ClickHouse is used), while persons, cohorts, and flags are small and mutable (PostHog, batch exports).

  • The API has a hard hourly ceiling. Analytics endpoints allow 240 requests a minute and 1,200 an hour per personal API key, with no higher tier offered (PostHog, API overview).

  • Persons cannot be read incrementally. The persons list filters by distinct ID, email, properties, and search text, never by modification time (PostHog, persons API).

  • Billing units differ by an order of magnitude, from rows exported or monthly active rows to nothing when you run the reader yourself.

  • Connection direction matters. A batch export connects from PostHog into your database over SSL with allowlisted IPs (PostHog, Postgres destination), while a self-run reader needs only outbound HTTPS.

How can you sync PostHog to Postgres?

Batch exports own the event stream, and every other path competes on the entity tables. The at-a-glance table shows what each path moves and what it costs.

Path

Events

Persons and flags

Billing unit

Runs where

Filament

Yes

13 entity resources

None

Your host

PostHog batch export

Yes

Persons and sessions only

Rows exported

PostHog cloud

Fivetran PostHog Lite

Via batch export endpoint

Yes, persons re-imported fully

Monthly active rows

Fivetran cloud

Airbyte

Yes, deprecated endpoint

Persons, flags, cohorts, insights

Self-hosted or credits

Your host or Airbyte Cloud

Estuary

Yes, query endpoint

Persons, cohorts, flags

GB plus per connector

Estuary cloud

ingestr

Yes, append

Persons, flags, cohorts

None

Your host

Sling

Yes, incremental

Persons, flags, cohorts

CLI Pro token

Your host

Plain script

Deprecated endpoint

Anything exposed

None

Your host

PostHog batch exports write three models, events, persons, and sessions, into your Postgres on a 5 minute to weekly schedule, with backfills (PostHog, batch exports). It reads ClickHouse directly, so no API ceiling applies. The limits are duplicates from overlapping backfills, new fields not added to existing tables, and no flags, cohorts, or experiments.

Fivetran lists PostHog as a Lite connector and swapped its EVENT table for BATCH_EXPORT_EVENT in July 2026 after PostHog discontinued the event endpoint (Fivetran, PostHog changelog). It syncs cohorts, flags, and insights incrementally and re-imports persons every sync (Fivetran, PostHog sync overview). It needs PostHog Cloud, so self-hosted is out, and it bills on monthly active rows (Fivetran, pricing).

Airbyte's PostHog source carries seven streams, with events incremental and persons, cohorts, flags, and insights full (Airbyte, PostHog source), community supported under the Elastic License (Airbyte, source metadata). It is one self-hosted connector for both shapes. Its events stream polls the deprecated endpoint, and the Postgres destination is advised for "small data volumes (e.g. less than 10GB)" (Airbyte, Postgres destination).

Estuary's capture snapshots persons and cohorts and reads flags, events, and sessions incrementally through the query endpoint every 5 minutes by default (Estuary, PostHog capture). That gives near-live events, but PostHog says connectors built on that endpoint "are not supported and will be rate-limited or rejected" (PostHog, query API). The connectors are BSL licensed (Estuary, LICENSE) and billed per gigabyte plus a monthly fee per connector (Estuary, pricing).

ingestr copies persons, flags, cohorts, events, and annotations with one command, merging persons on last seen time and appending events by timestamp (ingestr, PostHog source). Its license is the Functional Source License with a future Apache grant (ingestr, LICENSE), and the persons merge still pages the full list.

Sling exposes persons, events, cohorts, and flags as an API connection, with events incremental by timestamp, but "APIs require a CLI Pro token or Platform Plan" (Sling, PostHog connection). The CLI is GPLv3 (Sling, LICENSE). A plain script works for any resource, and dlt ships no verified PostHog source, so a dlt route is that same hand-written pull. Stitch and Hevo list no PostHog connector.

Where Filament fits on PostHog to Postgres

Filament carries the entity tables and leaves the event stream to the batch export. Its PostHog source reads 13 default resources, from persons and feature flags to experiments and surveys, and the docs state plainly that "Events are excluded" (Filament, PostHog source). Both connectors on this route are labeled beta (Filament, connector maturity). Filament is built by Galaxy, which also publishes this site.

Against the five facts above, the case is about control, not speed. No public benchmark run covers a PostHog route, and this series makes no speed claim without one. Each resource lands as its own typed table with primary keys preserved and JSON stored as jsonb, and re-runs upsert with ON CONFLICT (pk) DO UPDATE (Filament, Postgres sink). Schema evolution, once enabled, adds new source columns and never drops or retypes one.

Every batch carries a CRC32-C checksum that the sink recomputes before the COPY, and a failed run leaves progress resumable (Filament, integrity and checkpoints). The reader runs on your host, so no inbound route opens and no row meter runs.

The documented limits are real. Insights and session recordings are the only incremental resources, persons are "a full snapshot" on every run, and incremental reads do not propagate deletions (Filament, PostHog source). A static limiter of 0.3 requests per second keeps a run under the hourly bucket and sets the arithmetic below.

Persons in the project

Pages at 100 per page

Time at 0.3 requests a second

10,000

100

About 6 minutes

100,000

1,000

About 56 minutes

1,000,000

10,000

About 9 hours

These times are computed from the documented limiter and page size, not measured. Detail fetches for dashboards and experiments add one request per object.

How to sync PostHog to Postgres with Filament

One pipeline carries the entity tables on a nightly cron, and a PostHog batch export carries events into the same database. The commands follow the documented flag rule, where each connector field becomes a --source- or --sink- flag with underscores turned to hyphens (Filament, CLI guide). The docs show no PostHog example, so this sequence is assembled from that rule and is worth a filament source create --help check on v0.24.0.

Install the CLI, create a PostHog personal API key with read scopes for the resources you want, and export the key and your Postgres DSN as environment variables.

curl -fsSL https://getgalaxy.io/filament/install | sh
export POSTHOG_API_KEY=phx_...
export APP_DB_DSN=postgres://filament:...@db.internal:5432/analytics?sslmode

curl -fsSL https://getgalaxy.io/filament/install | sh
export POSTHOG_API_KEY=phx_...
export APP_DB_DSN=postgres://filament:...@db.internal:5432/analytics?sslmode

curl -fsSL https://getgalaxy.io/filament/install | sh
export POSTHOG_API_KEY=phx_...
export APP_DB_DSN=postgres://filament:...@db.internal:5432/analytics?sslmode

Register the project as a source. The host is the app origin, never the us.i.posthog.com ingestion host, and the project ID is the number after /project/ in the app URL.

filament source create product-analytics \
  --source-connector posthog \
  --source-api-key-env POSTHOG_API_KEY \
  --source-project-id 12345 \
  --source-host

filament source create product-analytics \
  --source-connector posthog \
  --source-api-key-env POSTHOG_API_KEY \
  --source-project-id 12345 \
  --source-host

filament source create product-analytics \
  --source-connector posthog \
  --source-api-key-env POSTHOG_API_KEY \
  --source-project-id 12345 \
  --source-host

Register the sink, then run discovery to confirm the resources resolve with your key's scopes.

filament sink create app-db \
  --sink-connector postgres \
  --sink-connection-method url \
  --sink-dsn-env APP_DB_DSN
filament source

filament sink create app-db \
  --sink-connector postgres \
  --sink-connection-method url \
  --sink-dsn-env APP_DB_DSN
filament source

filament sink create app-db \
  --sink-connector postgres \
  --sink-connection-method url \
  --sink-dsn-env APP_DB_DSN
filament source

Create the pipeline with upsert writes into a posthog schema and run it once.

filament pipeline create posthog-entities \
  --source product-analytics --sink app-db \
  --resources persons,cohorts,cohort_members,feature_flags,experiments,surveys \
  --sync-mode full --write-mode upsert \
  --sink-schema

filament pipeline create posthog-entities \
  --source product-analytics --sink app-db \
  --resources persons,cohorts,cohort_members,feature_flags,experiments,surveys \
  --sync-mode full --write-mode upsert \
  --sink-schema

filament pipeline create posthog-entities \
  --source product-analytics --sink app-db \
  --resources persons,cohorts,cohort_members,feature_flags,experiments,surveys \
  --sync-mode full --write-mode upsert \
  --sink-schema

The same setup as YAML in ~/.config/filament/filament.yaml.

version: 1
sources:
  product-analytics:
    type: posthog
    config:
      api_key: env:POSTHOG_API_KEY
      project_id: "12345"
      host: https://us.posthog.com
sinks:
  app-db:
    type: postgres
    config:
      connection_method: url
      dsn: env:APP_DB_DSN
      schema: posthog
pipelines:
  posthog-entities:
    source: { ref: product-analytics }
    sink: { ref: app-db }
    resources: [persons, cohorts, cohort_members, feature_flags, experiments, surveys]
    sync_mode: full
    write_mode

version: 1
sources:
  product-analytics:
    type: posthog
    config:
      api_key: env:POSTHOG_API_KEY
      project_id: "12345"
      host: https://us.posthog.com
sinks:
  app-db:
    type: postgres
    config:
      connection_method: url
      dsn: env:APP_DB_DSN
      schema: posthog
pipelines:
  posthog-entities:
    source: { ref: product-analytics }
    sink: { ref: app-db }
    resources: [persons, cohorts, cohort_members, feature_flags, experiments, surveys]
    sync_mode: full
    write_mode

version: 1
sources:
  product-analytics:
    type: posthog
    config:
      api_key: env:POSTHOG_API_KEY
      project_id: "12345"
      host: https://us.posthog.com
sinks:
  app-db:
    type: postgres
    config:
      connection_method: url
      dsn: env:APP_DB_DSN
      schema: posthog
pipelines:
  posthog-entities:
    source: { ref: product-analytics }
    sink: { ref: app-db }
    resources: [persons, cohorts, cohort_members, feature_flags, experiments, surveys]
    sync_mode: full
    write_mode

Verify from psql. Each table carries a raw column holding the fields the manifest did not project.

select table_name from information_schema.tables where table_schema = 'posthog';
select count(*) from posthog.persons;
select raw from posthog.persons limit 1

select table_name from information_schema.tables where table_schema = 'posthog';
select count(*) from posthog.persons;
select raw from posthog.persons limit 1

select table_name from information_schema.tables where table_schema = 'posthog';
select count(*) from posthog.persons;
select raw from posthog.persons limit 1

Schedule it. A pipeline takes one cron schedule with a timezone and an overlap policy, and SKIP stops a slow full read from stacking on itself (Filament, pipelines API).

Then create a PostHog batch export for events into the same database. Deduplicate on the event UUID, since overlapping backfills produce duplicates, and join on distinct_id rather than the event's person ID, which PostHog's changelog warns is often stale (PostHog, changelog).

The right path for each PostHog job

Land events with the batch export, and land everything else with a reader you run.

PostHog data you need

Path

Why it wins

Raw events, kept past retention

PostHog batch export

Only supported event path, reads ClickHouse, backfills

Flags, cohorts, experiments, surveys as SQL tables

Filament

Typed tables, add-only schema changes, no row meter, runs in your network

Events and entities from one managed vendor

Fivetran PostHog Lite

Batch export endpoint plus entity tables, if Cloud and MAR fit

Self-hosted PostHog with events and entities

Airbyte

Self-hosted, both shapes, accepts the deprecated endpoint risk

Sub-hourly events into a stream platform

Estuary

Five minute captures and many downstream materializations

A one-off copy from a laptop

ingestr

One command, no server

Fivetran wins when a team already pays for it and wants no extra reader. Airbyte wins for self-hosted PostHog, where neither the batch export nor Fivetran apply. Estuary wins when events must fan out beyond Postgres. Filament is the pick for the engineer who owns the Postgres, needs the entity tables typed and current, and will not pay by the row for them.

Related guides

Frequently asked questions

Can PostHog export events directly to Postgres?

Yes. PostHog batch exports write the events, persons, and sessions models to a Postgres table you own over SSL, on a schedule from every five minutes to weekly, with backfills. PostHog deprecated its events API and points connectors at batch exports, so this is the only supported path for raw events.

How much does a PostHog batch export to Postgres cost?

PostHog bills data pipelines per row exported. The first million rows each month are free, the next tier costs a fraction of a cent per row, and the rate falls at higher volumes. The receiving Postgres is yours, so the only other cost is the storage it uses.

What is the PostHog API rate limit for exports?

Analytics endpoints such as persons, insights, and session recordings allow 240 requests a minute and 1,200 an hour per personal API key. PostHog says it does not offer higher limits, so any connector that reads persons has to pace itself under the hourly ceiling or the run fails.

Can you sync PostHog persons incrementally?

No. The persons endpoint filters by distinct ID, email, properties, and search text but has no modified-since parameter, so every tool that copies persons reads the full list each run. Only the batch export persons model, which PostHog runs from its own ClickHouse, avoids the API entirely.

Why does PostHog recommend against Fivetran and Airbyte for exports?

PostHog's docs call those connectors unofficial and do not recommend them for moving data to a warehouse, because they poll an API built for the app, not for bulk export. The events endpoint is deprecated and the query endpoint rejects bulk exports. Batch exports read ClickHouse directly and are the supported route.

Which PostHog data can you get into Postgres besides events?

Persons, cohorts, feature flags, experiments, surveys, insights, dashboards, actions, annotations, notebooks, and session recording metadata are all exposed by the project API. Connector tools copy some or all of them as tables. Replay media and fresh insight calculations are not available, only metadata and cached results.

Does Filament sync PostHog events to Postgres?

No. The Filament PostHog source reads thirteen entity resources, including persons, cohorts, feature flags, experiments, and surveys, and its documentation says events are excluded. Pair it with a PostHog batch export for the event stream. Insights and session recordings read incrementally, and everything else is a full read each run.

How long does a Filament PostHog run take?

Filament paces PostHog requests at 0.3 per second to stay under the hourly analytics bucket, reading a page of persons per request. A project with 100,000 persons takes about an hour and a million takes most of a working day, before detail fetches for dashboards and experiments. Interrupted runs resume after the last completed page.

More articles

Stay up to date with what weโ€™re building

Stay up to date with what weโ€™re building

Stay up to date with what weโ€™re building

Questions

Answered

FAQ

What does Galaxy do?

What is Filament?

What is enterprise context management?

What does working with Galaxy look like?

How do you handle security and compliance?

Why does Galaxy build in the open?

Own your knowledge stack

Own your knowledge stack

Copyright ยฉ 2026 Galaxy. All rights reserved.