Fathom vs Matomo: Who Runs the Server, and What It Costs Your CSP
Hosted or self-hosted is the real axis. The CSP cost of each, measured against a live policy, plus why script weight is the wrong thing to compare.
Both Fathom and Matomo exist because Google Analytics asks you to put a consent banner on your own site. Both are cookieless-capable and GDPR-friendly. The difference that decides it is not the dashboard: Fathom is a hosted product you buy, and Matomo is a platform you can run yourself — which means one of them adds a subscription and the other adds a server.
The short answer
Choose Fathom if you want privacy-friendly pageviews with no infrastructure to operate and a dashboard nobody needs training for. Choose Matomo if you need session-level depth (funnels, heatmaps, visitor profiles, goals), or if "the data must live on hardware we control" is a requirement rather than a preference — Matomo self-hosts, Fathom does not.
Everything below that is a consequence of those two sentences.
The axes that actually diverge
| Fathom Analytics | Matomo | |
|---|---|---|
| Who runs it | Fathom — hosted only | You (self-hosted) or Matomo Cloud |
| Self-host the full product | No | Yes |
| Source model | Proprietary (a cut-down Lite edition is open source) | Open source |
| Install | A script tag carrying a site ID | A script pointed at your Matomo instance |
| Data depth | Pageviews, referrers, goals | Funnels, heatmaps, session recording, goals, A/B testing, an API |
| Data location | Fathom's infrastructure | Your server, or Matomo's cloud |
| Cookies | Cookieless by default | Configurable; cookieless mode available |
| Consent banner | Not required for the default setup | Depends which features you enable |
| CSP cost | One script host + one beacon host, or zero cross-origin via custom-domain routing | Your own origin (self-hosted) or Matomo's (cloud), on script-src and connect-src |
| Ongoing work | None | Updates, backups and uptime for an analytics server, if self-hosted |
| Portable off your host | Yes | Yes |
Pricing on both moves often enough that quoting today's numbers would mislead you by the time you read this. Check their own pricing pages, and compare on pageviews or events per month rather than on seat count — that is the axis both bill on.
Matomo Cloud narrows the infrastructure gap to nothing, for a subscription. What it does not narrow is the depth gap or the CSP cost: it is the same tracking script and the same dashboard, just not one you patch.
Disclosure, because it changes how to read this
This storefront runs neither. It runs Vercel Web Analytics, because it deploys on Vercel and its entire reporting need is five named conversion events. We evaluated both of the tools above during that decision, which is where the measurements below come from — but nobody should take "we picked a third thing" as a verdict on either.
If your situation is close to ours, the two posts that cover those matchups directly are Fathom vs Vercel Analytics and Vercel Analytics vs Matomo. This post is for the case where Vercel is not in the picture and the real question is hosted-versus-self-hosted.
The cost neither vendor's comparison page mentions: your CSP
If you ship a Content-Security-Policy, adding analytics is a policy edit, and the size of that edit differs between these two. Here is the live policy from this project's next.config.ts, unabridged:
const contentSecurityPolicy = [
"default-src 'self'",
`script-src 'self' 'unsafe-inline'${scriptSrcEval} https://app.lemonsqueezy.com https://assets.lemonsqueezy.com https://va.vercel-scripts.com`,
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob: https:",
"font-src 'self' data:",
`connect-src 'self' ${supabaseOrigin}`,
"frame-src 'self' https://*.lemonsqueezy.com https://*.asoctemplates.com https://*.vercel.app",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self' https://*.lemonsqueezy.com",
"object-src 'none'",
].join("; ");
Two directives matter for analytics, and a tracker almost always wants both: script-src to load its JavaScript, and connect-src for the beacon that reports events back. The file's own rule is "each verified against the code that loads it — do not add speculative hosts," which is the discipline that makes this comparison concrete:
- Fathom, default install: one
script-srchost for the script, and aconnect-srcentry for the collector. Two allowances. - Fathom with custom-domain routing: you point a subdomain of your own at their collector. Both the script and the beacon become first-party, so
'self'already covers them — zero new hosts, and as a side effect the requests are much harder for a blocker to drop. - Matomo, self-hosted: your Matomo instance is your own origin, so the same zero-new-host logic applies if it lives on a subdomain of the site. That is self-hosting's quiet CSP win.
- Matomo Cloud: your instance's cloud hostname, on both directives.
The pattern: whichever tool you can serve from your own domain is the one your policy likes, and both of these offer a route to that. The tool that cannot be made first-party is the one you end up writing exceptions for.
One more directive is worth checking before you commit: connect-src also governs WebSockets. This policy deliberately has no wss: entry, documented in the file as "No realtime is used (no .channel( anywhere), so no wss: is needed." If a tool you are evaluating opens a socket for live-visitor views, that is another allowance, and it is the kind of thing you discover from a console error on a Friday rather than from a feature list.
Script weight is the wrong axis, and we can show why
Comparisons of privacy analytics love the script-size table — one tool's tracker is a few kilobytes, another's is tens. It is a real difference and, on a React site, it is usually irrelevant.
The measured reason, from this project's Lighthouse pass: mobile performance sits at 89–99 across the eight main pages while desktop is 100 everywhere, and the remaining mobile gap is not bytes on the wire. With every image and every prefetch blocked, the home page's LCP moved from 3786 ms to 3740 ms — 46 ms. The cost is JavaScript execution, and the single largest chunk is react-dom at 221 KiB raw / 70 KiB gzipped. That is the framework floor for an interactive storefront.
Against that floor, the delta between two analytics scripts is noise. Weigh instead:
- Whether it is render-blocking. Any tracker loaded
async/deferfrom a fast host costs essentially nothing; one that is not will cost more than its file size suggests. - Whether it is same-origin. A third-party DNS lookup and TLS handshake on a cold connection is usually worth more milliseconds than the script.
- Whether its failure is visible. Which brings us to the one audit this site does not score 100 on.
The 96 that is really an analytics finding
Accessibility and SEO are 100 on every page, both form factors. Desktop performance is 100 across the board. Best Practices is 96, and the single failing audit is errors-in-console — entirely the Vercel Web Analytics beacon 404-ing when the production build runs on localhost, because the script is injected at the edge only on a real deployment. On the deployed site it returns to 100.
It is a local-measurement artifact, and it is also the general lesson about third-party analytics: when the collector is unreachable, the failure surfaces in your console and in your audits, not in the vendor's dashboard. Every environment where the tracker cannot phone home — local builds, CI, a preview behind auth, a visitor with a blocker — produces that noise. Both tools here have a first-party answer (custom-domain routing; self-hosting on a subdomain), and that is a better reason to use those features than blocker evasion.
What you are actually buying with Matomo's depth
Matomo's feature list is longer in a way that is easy to over-value. The honest test is whether your event vocabulary needs session-level analysis at all.
Ours does not. The whole analytics surface of this site is one typed union in src/lib/analytics.ts — five conversion events (preview_opened, buy_clicked, waitlist_joined, blog_cta_clicked, wishlist_added), each carrying a couple of string properties, wrapped so a typo cannot invent a sixth and so a throw inside a click handler can never cost the sale it was measuring. For a catalogue of 111 products and a few hundred articles, the question we need answered is "which article sent a buyer to which product page", and that is a group-by on two properties, not a funnel.
If your questions sound like where in the five-step signup do people drop out, what did the user click before rage-quitting, or which variant won — Matomo answers those and Fathom does not pretend to. If they sound like how many people read this and clicked through, Matomo's depth is a server you maintain for reports you will not open.
A useful exercise before choosing either: write your event list down. If it fits on one screen, buy the simple tool.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Script blocked, Refused to load in console | The tracker's host is not in script-src | Add the host, or serve it first-party via custom-domain routing / self-hosting |
| Script loads, no data arrives | The beacon destination is missing from connect-src | Add the collector host; it is a separate directive from the script |
| Console 404s for the tracker in local builds | The collector only exists in the deployed environment | Expected; it is also why a local Best Practices score can read low |
| Numbers much lower than server logs | Blockers dropping third-party requests | Serve the tracker from your own domain — both tools support this |
| Self-hosted Matomo slows down over time | Raw data growth plus archiving on request | Run the archive cron job; it is not optional at traffic |
| A "cookieless" setup still triggers a consent review | An enabled feature reintroduced identifiers | Audit which features you turned on; cookieless is a configuration, not a brand |
| Live-visitor view silently empty under CSP | A WebSocket blocked by connect-src | Allow the wss: origin explicitly, or drop the feature |
| Analytics works locally, missing in production | The site ID lives in a build-time env var that was never set on the host | Set it in the hosting project and rebuild — see below |
Frequently asked questions
Is Matomo free? Self-hosted Matomo is free to download and open source; it is not free to run. The bill moves from a subscription line to a server, a database, updates, backups and the archiving job. Matomo Cloud exists precisely for teams that want the features without that. Fathom has no free self-hosted path to the full product.
Which one is better for GDPR? Both are built for it, and neither makes you compliant by itself. Self-hosted Matomo gives you the strongest story available here — the data never leaves infrastructure you control — but you inherit responsibility for securing it. Fathom's default is cookieless and anonymous, which for most sites removes the banner. Read each vendor's current data-processing terms; this is the one area where a blog post is the wrong source.
Will my site ID end up in the browser?
Yes, and that is fine — it is in the script tag either way. What matters in a framework app is when it gets there. In Next.js a NEXT_PUBLIC_* value is inlined into the bundle when the build runs, so changing it on your host does nothing until you rebuild. That trips people up on exactly this kind of integration; the Next.js env rules walk through the three moments a value is read.
Can I run both while deciding? For a few weeks, yes, and it is the only way to compare numbers you will actually trust. Expect the totals to disagree — different bot filtering, different session rules, different blocker exposure. Compare trends and relative page rankings, not absolute visitor counts.
Templates in this post
ASoc Lens is a product-analytics SaaS marketing site — the closest thing in the catalogue to the product you are shopping for. ASoc Ledger (a finance-app site with live dashboards) and ASoc Magnet (a lead-capture site with channel insights) ship the same CSP-friendly structure: no third-party script hosts in the default policy, so adding your tracker is one reviewed line.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates. For the other common pairing, see Vercel Analytics vs Google Analytics.
