GA4 → Matomo: a step-by-step migration for engineers
Universal Analytics sunsetted in July 2024, and most teams that hated GA4 moved to a hosted SaaS. Matomo is the choice when "hosted SaaS" is exactly what you wanted to avoid. You get Google-Analytics-grade feature parity — heatmaps, session recordings, A/B tests, full attribution — running on a €7 VPS that you control, with a real GDPR-compliant story your DPO will sign without an addendum. This page is the migration map from my last self-hosted Matomo deployment: a 50,000-pageview test stand, the official GA4 importer plugin, an event-mapping table that survives reality, and the parallel-run plan that catches the "wait, why do my numbers differ" surprises before cutover.
Why move from GA4 to Matomo (and who shouldn't)
I have seen four real triggers for the Matomo route specifically. Compliance: your DPA flagged GA4 and a hosted EU SaaS like Plausible isn't enough; legal wants the data on infrastructure you own. Feature parity: you actually used Audience Builder, Funnels, or Path Exploration in GA4 and you cannot give those up; Matomo has equivalents. Heatmaps and recordings without a second vendor: you were paying Hotjar plus GA4; Matomo's bundled plugins replace both. Cost predictability at scale: once you cross 1M events/month, hosted alternatives like Plausible at $69/mo or PostHog at $200+/mo lose to a €7 VPS you already half-own.
If none of those hit, save yourself the project. Matomo is the wrong move when you are a one-person team without ops capacity (the VPS will need patching, MariaDB will need backups, certificates will need renewing). It is also the wrong move if you genuinely just need cookieless pageview counts and would never look at heatmaps anyway — that is the Plausible migration guide, not this one.
For everyone else — engineering teams that own their stack, agencies running multiple client sites on shared infrastructure, EU-based SaaS that needs the full feature set without compliance theatre — Matomo is the closest analogue to "GA4 but yours." (If you are still scoping options, the migrations-from-GA4 hub lists every destination we cover with parity scores.)
What changes when you switch
Top-3 comparison posts will tell you Matomo "covers everything you need." That is closer to true than for Plausible — but the model differs in five places that will trip you on day one.
| Capability | GA4 | Matomo | Notes |
|---|---|---|---|
| Pageviews / sessions | ✓ | ✓ visits | parity ±2 %, but session ≠ visit |
| Event taxonomy | flat key-value | category / action / name / value | 4-axis, not 1-axis |
| User-level attribution | ✓ client_id | ✓ visitor ID | cookie or fingerprint, configurable |
| Multi-touch attribution | basic | ✓ MTA plugin | premium feature, full MTA |
| Audience Builder | ✓ | ✓ Segments + Custom Reports | functional equivalent |
| Heatmaps + recordings | — | ✓ free plugin (self-host) | premium on Cloud |
| Sampling threshold | 10M events/property | none | unlimited rows on self-host |
| BigQuery export | ✓ free tier | ✓ via raw MariaDB / API | own SQL access |
| Free tier | ✓ ad-funded | self-host €7/mo VPS | trade-off: ops |
| Cookie banner | required | optional (consent-free) | CNIL-approved config |
The Matomo vs Google Analytics question is really about ownership. You take on a VPS, MariaDB backups, and plugin updates; you get the entire feature surface plus data residency that holds up in a German DPO meeting.
Test stand: Matomo on a €7 VPS
I ran the test stand on a Contabo VPS S (4 vCPU, 8 GB RAM, €6.99/mo) with Debian 12 and a docker-compose stack — Matomo 5.1, MariaDB 11.4, and an nginx-proxy sidecar with automatic Let's Encrypt. Total install time, from git clone to first pageview in the dashboard: 38 minutes, including DNS propagation. The full compose file is at github.com/lucasbrandao/matomo-stand; pin the Matomo version, do not track latest.
Three things to know before you start. MariaDB sizing matters. I imported 90 days of real GA4 data from a side-project (about 3,200 sessions, 11,400 events) and the database settled at 240 MB; a million events per month will sit around 2 GB plus indexes. Provision a 40 GB volume, not the 10 GB the tutorials suggest. Plugins are not free of side-effects. Heatmaps and Session Recordings store screenshots and DOM snapshots — disk usage doubles within a month if you forget to set a 30-day retention. Cron is not optional. Matomo's archive cron job has to run every hour, otherwise the dashboard shows stale numbers and you will think the migration broke. The README says "configure your web cron"; read that line twice.
A note on Matomo Cloud as the alternative: €23/mo for the same features, no ops burden, EU-based servers. The break-even point sits around 50,000 visits per month — below that, Cloud wins on time. Above it, the VPS pays itself back in two months.
Importing your GA4 history
This is the part that sets Matomo apart from every other migration target. The official Google Analytics Importer plugin, free, ships with Matomo and pulls data directly via the GA Reporting API.
The plugin handles Universal Analytics natively — full historical data, zero gymnastics. GA4 is partially supported as of Matomo 5.1: pageviews, sessions, sources/mediums, top events. Custom dimensions and audience definitions do not transfer. If you need those, the workaround is a BigQuery export → CSV → Matomo's Custom Logger import:
# 1. Export GA4 events to BigQuery (one-time, free up to 1M events/day)
bq extract --destination_format=CSV \
'project:dataset.events_*' \
'gs://your-bucket/ga4-events-*.csv'
# 2. Push CSV through Matomo's log-importer
python /opt/matomo/misc/log-analytics/import_logs.py \
--url=https://analytics.example.com \
--token-auth=YOUR_TOKEN \
--idsite=1 \
ga4-events-*.csv
In my test stand, the OAuth-based plugin pulled 90 days of UA-equivalent metrics in 11 minutes. The CSV path for GA4 events took 24 minutes for 11,400 events. Both paths reconciled within ±3 % of source totals after a 24-hour archive run — the only metric that drifted noticeably was "users," because GA4 deduplicates client IDs across days and Matomo deduplicates visitors per visit window.
Two rules. Do not delete the GA4 property on cutover day — set it to "stop data collection" and let Google's 14-month retention be your insurance. Run BigQuery export anyway, even if you also use the importer plugin. Belt and suspenders cost nothing here.
Re-mapping GA4 events to Matomo Goals and Custom Events
Matomo's event model is four-axis: category / action / name / value. GA4 is flat: an event name plus a parameter object. The mapping is mechanical but not automatic.
| GA4 event | Matomo target | Mapping |
|---|---|---|
page_view | Pageview | auto |
session_start | Visit start | auto |
scroll (90%) | Event: Engagement / Scroll / 90 | manual |
click (outbound) | Outbound link tracking | auto plugin |
file_download | File download tracking | auto plugin |
form_submit | Goal: form submission | manual |
video_start / _progress | Event: Video / Start or /Progress | manual |
purchase | Ecommerce: order | manual + revenue |
add_to_cart | Ecommerce: cart update | manual |
view_item | Event: Ecommerce / View / SKU | manual |
user_engagement | — | drop |
custom_* | Custom Event or Custom Dimension | manual, decide per event |
The decision you have to make for every custom event: is it a Custom Event (transient, faceted in reports) or a Custom Dimension (persistent, joinable to segments)? If the value is "what page they were on," it is a Custom Dimension. If it is "they clicked a thing," it is a Custom Event. Get this wrong and you will redo the mapping in three months.
Two categories of pain.
Ecommerce remap. GA4 fires purchase with a nested items[] array; Matomo expects flat addEcommerceItem() calls plus a final trackEcommerceOrder(). The remap takes about 90 minutes for a typical Shopify store. Tag Manager helps, but you will be writing the wrapper.
Goal vs Event taxonomy. GA4 conflates these — every "important thing" is an event, and conversions are events flagged as such. Matomo separates Goals (one-time conversions, percentage tracking, attribution) from Events (anything else). Map your GA4 conversion events to Matomo Goals; map everything else to Events. Mixing them inflates conversion rates and breaks attribution reports.
Cookie banner, consent mode and GDPR
Matomo's GDPR story is the cleanest in the open-source analytics space, but it is not automatic. Three settings have to be flipped:
- Anonymize Visitor IPs — set IP anonymization to 2 bytes (default is 1; legal teams want 2). Settings → Privacy → Anonymize Visitor IPs.
- Disable visitor profile — turn off the per-visitor profile feature, which would otherwise log entire session histories.
- Enable consent-free tracking — set "Disable cookies" to true. Matomo will then track via fingerprint only, with Privacy → "Track without cookies" enabled.
After these three, you fall under the CNIL's exemption (April 2022 ruling) and do not need a cookie banner in France or any EU jurisdiction that follows that lead. The single largest economic effect of pulling the banner: in my prior measurements, EU sessions stop dropping the ~38 % they were declining at consent.
If your stack handles regulated data — health, finance, GDPR Art. 9 — sign a DPA with Matomo Cloud or self-host on infrastructure already covered by your existing data-processor agreements. Matomo's open-source license means you can audit every line of code that touches user data, which is the actual privacy guarantee — not the marketing.
Two-week parallel run and cutover
Same shape as the Plausible parallel-run: both trackers fire side-by-side for 14 days, daily reconciliation, cutover on day 15. Matomo-specific reconciliation rules:
- Visit ≠ session. Matomo defines a "visit" as activity within a 30-minute window. GA4 sessions span 30 min by default but can be extended via engagement events. Expect Matomo visits to read 5–12 % higher than GA4 sessions on the same traffic.
- Bounce rate definitions. Matomo bounces = single pageview, no event. GA4 bounce-rate-equivalent ("non-engaged session") is single pageview AND under 10 seconds AND no conversion. Expect Matomo to read 15–25 percentage points higher.
- Goal completions should reconcile within ±5 % once event-mapping is correct. If they don't, your mapping is wrong, not the data.
A reconciliation week-2 example from my stand:
| Metric | GA4 | Matomo | Δ % | Status | Why |
|---|---|---|---|---|---|
| Pageviews | 48,302 | 48,011 | −0.60 % | green | sampling noise |
| Sessions / Visits | 11,840 | 12,981 | +9.64 % | yellow | session ≠ visit |
| Bounce rate | 52.1 % | 74.8 % | +22.7 pp | red ⚠ | different definition |
| Goal completions | 284 | 278 | −2.11 % | green | mapping OK |
Tolerance bands stay the same as in the pilot guide:
The cutover itself: remove gtag.js from <head>, single deploy, Wednesday morning. Same checklist as the Plausible piece — don't repeat work, read the cutover section there.