
Matomo Analytics: Privacy Is a Configuration, Not a Feature
Most people install Matomo for one reason: they want web analytics without handing their visitors to Google. That reason is good, and it is also where the thinking usually stops. The tracking code goes on the site, the dashboard fills up, and a year later nobody has enabled the one setting that would have made the installation lawful in the first place, nobody has set up the cron job that keeps the reports from crawling, and the raw data everyone was so keen to own is quietly being deleted at ninety days by a default nobody read.
1. The Basics
Matomo is a web analytics platform: it records what visitors do on your website and turns that into reports. In that much it is the same job Google Analytics does. What makes it worth a separate article is not the reports. It is where the data physically sits, and what that single structural difference changes about everything else.
Matomo is open source, published under the GPL. You can read every line that processes your visitors' data, which is an unusual property for an analytics tool. It matters more than it sounds: it is the difference between a privacy claim you accept and one you can confirm. Section 7.1 works through a claim both kinds of tool make, and shows what checking it looks like when the database is yours.
Analytics you host yourself is not automatically private analytics. It is analytics whose privacy is now your job.
This article starts with the simplest question you can ask any analytics tool ("is anything arriving at all?") and builds up to the Reporting API, server log import, and the archiving cron job that decides whether a busy Matomo is fast or unusable. Along the way it takes apart the three things people get wrong most: what "privacy-friendly" actually requires you to switch off, why unique visitors are missing from your yearly report, and why raw data is both the best reason to run Matomo and the most expensive thing about it.
The right mental model: Matomo is not a service you subscribe to, it is a database you operate. Every advantage it has over a hosted tool, and every way it goes wrong, follows from that one fact.
1.1 What It Is Not
Getting the negatives right saves a lot of confusion later:
- It is not automatically GDPR compliant. This is the single most damaging thing people believe about Matomo. It ships with privacy features that most tools do not have. It does not ship with all of them enabled, and section 7.4 goes through what "compliant" actually costs you in features.
- It is not a search tool. It measures visitors on your site. It knows nothing about how Google crawls or indexes you. That is a different tool with a different data source, and section 7.8 covers where the boundary sits.
- It is not free in every sense. Matomo On-Premise costs nothing to license. It costs a server, a database, a cron job, and someone to update it. Matomo Cloud reverses that trade.
- It is not cookieless by default. The standard JavaScript tracker sets first-party cookies. Cookieless is a mode you turn on, and it costs you accuracy in specific, listed reports.
- It is not a drop-in Google Analytics replacement. The data model is different enough that your old numbers and your new numbers will not agree, and section 9 explains why that is expected rather than a fault.
1.2 The One Structural Difference
With a hosted analytics service, your visitors' data goes to a third party, that third party aggregates it, and you are given reports back. You never touch the underlying rows. With Matomo On-Premise, the tracking request from your visitor's browser arrives at your server, is written to your MySQL database, and is aggregated by your PHP process.
HOSTED ANALYTICS
visitor → script on your page → vendor's servers
the vendor stores the rows, you get reports
MATOMO ON-PREMISE
visitor → script on your page → YOUR server, YOUR MySQL
you store the rows, you build the reports,
you are responsible for all of it
MATOMO CLOUD
visitor → script on your page → Matomo's servers
same software, someone else runs it
Everything distinctive about Matomo comes out of that middle line. No sampling, because there is no reason for your own server to sample your own data. Access to individual visits, because the rows are right there. Unlimited custom reporting, because you can query the database directly. And equally: a slow dashboard when the server is undersized, a database that grows without limit, and a legal position where you are the one holding the personal data.
1.3 Two Ways to Run It
| Matomo On-Premise | Matomo Cloud | |
|---|---|---|
| Where the data sits | Your server, your database | Matomo's infrastructure, hosted in Germany |
| Licence cost | Free, unlimited sites and hits | Subscription, priced by monthly hits |
| Updates, backups, archiving | Yours to run | Handled for you |
| Database access | Full SQL access | API and exports only |
| Premium plugins | Sold separately, installed from the Marketplace | Included depending on plan |
| Data controller | You, entirely | You, with Matomo as processor |
The core product is the same software in both cases. Prices and plan contents change, so check matomo.org/pricing rather than any figure quoted in an article; this one was checked in August 2026.
One thing the table understates, and it is the most common budgeting mistake with this product: "Matomo On-Premise is free" is true of the core and not of the product most people picture. The core is genuinely complete analytics, free for unlimited sites and unlimited hits, with no sampling and no upsell inside the reports. But A/B Testing, Heatmaps, Session Recording, Form Analytics, Funnels, Cohorts, Crash Analytics, Roll-Up Reporting and SAML/LDAP login are commercial plugins, sold as annual subscriptions priced by user tier, individually or in bundles.
They are priced for businesses, not for hobby sites. A single premium plugin's yearly subscription routinely exceeds the yearly cost of the server hosting the entire installation, and the larger bundles are enterprise software with enterprise numbers; some are no longer listed publicly at all and route you to a sales conversation instead. Treat them as software subscriptions in the budget rather than as extras, and check the current price before promising a client heatmaps or A/B testing. The saving from self-hosting is real, and it is a saving on the core, which is a smaller thing than "free" suggests.
One difference is not obvious from a pricing page and matters more than most of the table: some settings that exist On-Premise cannot be reached on Cloud, because they are configuration-file settings rather than interface settings. Section 7.3 shows the one that catches people out.
1.4 Log Data and Report Data Are Two Different Things
This is the distinction that makes Matomo's behaviour readable, and almost nobody is told about it before they need it.
Matomo stores your analytics twice, in two different shapes:
| Log data (raw) | Archive data (reports) | |
|---|---|---|
| What it is | One row per visit and per action, exactly as tracked | Pre-calculated totals for a site, a period, and a segment |
| Tables | log_visit, log_link_visit_action, log_action, log_conversion |
archive_numeric_YYYY_MM, archive_blob_YYYY_MM |
| Written by | The tracker, on every hit | The archiving process, afterwards |
| Powers | Visits Log, Visitor Profile, new segments, heatmaps | Every dashboard number you normally look at |
| Size | Large and always growing | Comparatively small |
Reports are not calculated when you open them. They are calculated in advance from the log data, stored in the archive tables, and then simply read back. That process is called archiving, and section 6.1 is about it, because on a self-hosted Matomo it is the difference between a tool that answers in a second and one that times out.
Keep the two apart in your head and several confusing behaviours become obvious at once. Deleting old raw data does not delete your reports. Creating a new segment can show nothing at first, because no archive exists for it yet. And a report you have never opened may not exist anywhere yet, even though the visits it describes were tracked months ago.
Back to top2. Where the Name Comes From
The product has had two names, and both stories are short and worth knowing, because the second one explains the project's whole position.
It began as Piwik. Its founder, Matthieu Aubry, wanted a name that was unique, easy to remember, easy to pronounce, with as few letters as possible, and with a free domain. He was inspired by the word "kiwi" and made up "Piwik" from it. The name means nothing at all. That was the point: it was available.
On 9 January 2018, after roughly ten years and more than a million websites, Piwik became Matomo. The project's own explanation for the change was that it wanted a name nobody else was using, to remove any possible confusion or affiliation, and one that suited what it had become rather than what it was in 2007.
The chosen word is Japanese. Matomo's announcement puts it plainly: "Matomo means decent in Japanese. Which aligns with one of our key values - transparency."
Piwik 2007 invented from "kiwi", no meaning, domain was free
Matomo 2018 Japanese for "decent"; chosen for what it says
That is the difference between the two names in one line. The first was picked because it was available. The second was picked because it was an argument. A product that competes on privacy chose a word meaning "decent" and then spent the following years being measured against it, which is a harder promise to keep than a made-up name ever asks for.
The old name has not entirely gone. You will still meet piwik in the wild: in the _pk_ prefix on every cookie the tracker sets, in the _paq JavaScript queue variable, in old documentation URLs, and in older integrations that still post to piwik.php. Matomo kept those aliases working deliberately, because renaming them would have broken a million installations at once.
3. A Short History
Matomo is older than most people assume, and its shape today is the result of two long-running pressures: catching up with a free competitor that had far more engineers, and betting the product on a privacy position years before that position was commercially obvious.
| Date | Milestone |
|---|---|
| June 2007 | The project starts in London. Matthieu Aubry, then an engineering student, had already written an earlier analytics tool called phpMyVisites |
| September 2007 | First alpha release |
| March 2008 | First public version, 0.1.1 |
| September 2009 | Wins "Best of open source enterprise software" in the web analytics category |
| August 2010 | Piwik 1.0, and the first mobile app |
| July 2011 | Ecommerce analytics, custom variables, and first-party cookies |
| June 2012 | One million downloads |
| December 2013 | Piwik 2.0 and a redesign |
| December 2016 | Piwik 3.0 |
| 9 January 2018 | Renamed Matomo |
| June 2018 | Matomo Tag Manager launches, included at no extra cost |
| October 2021 | France's data protection authority, the CNIL, lists Matomo among the solutions that can be configured to qualify for its consent exemption |
| December 2023 | Matomo 5.0 |
| April 2026 | One-click CNIL compliance: the exemption checklist becomes an assessment screen inside Matomo |
| 16 August 2026 | Matomo 5.13.0, the current stable release at the time of writing |
Three entries in that table carry more weight than the rest.
First-party cookies in 2011 looks like a footnote and was a structural decision. Third-party cookies were still ordinary then, and every browser vendor spent the following decade killing them. Matomo had already moved.
The 2018 rename was the moment the project stopped describing itself as a Google Analytics clone you could host and started describing itself as the privacy option. Everything after it, including the CNIL work, follows from that decision.
The 2021 CNIL listing is the only entry in the table that comes from outside the project. A national regulator publishing a configuration guide for a specific analytics tool is unusual, and it turned "privacy-friendly" from a marketing adjective into a checklist that can be passed or failed. Section 7.4 works through that checklist, because the interesting part is how much of the product it asks you to switch off.
Back to top4. Simple Use Cases
4.1 The Simplest Useful Question: Is Anything Arriving?
Before any report means anything, confirm that tracking requests reach the server at all. Matomo has a report for exactly this, and it is the first place to look when a dashboard is empty: Visitors → Visits Log. It shows individual visits as they arrive, within seconds.
If the Visits Log is empty while you are browsing your own site in another tab, the problem is never the reports. It is one of four things, in this order of likelihood:
1. the tracking code is not on the page → view source, search for "_paq"
2. the tracker URL is wrong or blocked → browser devtools, Network tab,
look for matomo.php and its status
3. you are excluding yourself → Administration > Websites > Settings,
excluded IPs; or an opt-out cookie
4. a content blocker is stopping the request → test in a clean browser profile
Point 3 catches experienced people constantly. You set an IP exclusion months ago, your address changed, and now you are excluding someone else while merrily tracking yourself.
4.2 The Tracking Code
Matomo's JavaScript tracker is a small asynchronous snippet. It queues instructions in an array called _paq and then loads matomo.js, so the browser never waits for it before rendering the page.
<script>
var _paq = window._paq = window._paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u = "https://analytics.example.com/";
_paq.push(['setTrackerUrl', u + 'matomo.php']);
_paq.push(['setSiteId', '1']);
var d = document, g = d.createElement('script'),
s = d.getElementsByTagName('script')[0];
g.async = true; g.src = u + 'matomo.js';
s.parentNode.insertBefore(g, s);
})();
</script>
Two lines do the actual work. trackPageView records the pageview. enableLinkTracking turns on automatic tracking of downloads and outbound links, which is why Matomo can report those without you tagging anything.
The queue design is worth understanding, because it explains a whole class of "my custom tracking does not work" problems. _paq is a plain array that exists before matomo.js loads. Anything you push into it is replayed in order once the tracker is ready. So order matters: configuration calls such as setUserId or setCustomDimension must be pushed before trackPageView, or they will not be attached to that pageview.
_paq.push(['setCustomDimension', 1, 'member']); // correct: set first
_paq.push(['trackPageView']);
_paq.push(['trackPageView']);
_paq.push(['setCustomDimension', 1, 'member']); // wrong: pageview already sent
4.3 The Visits Log, and Why It Does Not Exist Elsewhere
Most analytics tools show you aggregates: 1,240 visits, 3.2 pages each. Matomo shows you those too, and it also lets you open a single visit and read it action by action, with timestamps, referrer, device, and the exact path the person took through the site.
Two reports do this. Visits Log lists visits one by one. Visitor Profile takes one visitor identifier and shows every visit that identifier has ever made.
This is genuinely useful. When a form stops converting, an aggregate tells you conversions fell; the Visits Log shows you eleven people reaching step three and leaving, which turns a statistic into a bug report.
It is also the feature with the largest privacy cost in the product, and the two facts are the same fact. A per-visitor history is exactly what makes the data useful for diagnosis, and exactly what makes it personal data. This is why the CNIL exemption in section 7.4 requires you to switch both reports off. They are the first thing a regulator looks at and the first thing you will miss.
4.4 What Counts as a Visit
Every number in the dashboard rests on a definition, and this is the one to know. By default, Matomo starts a new visit when a visitor performs an action more than 30 minutes after their last tracked interaction.
The setting lives in the tracker configuration, in seconds:
[Tracker]
visit_standard_length = 1800 ; 30 minutes, the default
Change it and every derived metric changes with it. A longer window produces fewer visits, a lower bounce rate, and longer average visit durations. Nothing about your visitors changed. You can defend a longer window for a documentation site people read slowly, but you can no longer compare your numbers to anyone else's, or to your own from before the change.
4.5 Goals
An analytics tool that only counts pageviews tells you about traffic, not about outcomes. A goal is a definition of something worth happening: a form submitted, a file downloaded, a thank-you page reached, a click on a mail link.
You define goals under Goals > Manage Goals, and each one can be triggered by a URL pattern, a page title, an event, or an explicit call in your own code:
_paq.push(['trackGoal', 3]); // goal 3 converted
_paq.push(['trackGoal', 3, 49.95]); // ... with a revenue value
Once a goal exists, every other report gains a column. You stop asking which channel sends the most visitors and start asking which one sends visitors who do the thing. Those are usually different answers, and only the second one is worth acting on.
One warning for later: goals are the point where analytics stops being audience measurement and starts being something a regulator treats differently. Section 7.4 returns to it.
4.6 Beyond Pageviews: Events, Site Search, and Custom Dimensions
A pageview is recorded when a page loads. Modern sites are full of things that matter and load no page: an accordion opening, an AJAX form submitting, a video playing, a call-to-action being clicked. Matomo records those as events.
_paq.push(['trackEvent', 'Contact', 'Submit', 'Quote form']);
category action name (optional)
_paq.push(['trackEvent', 'Video', 'Play', 'Intro', 42]);
value (optional)
Site searches get their own call, because a search is worth more than a pageview: it is a visitor telling you in their own words what they could not find.
_paq.push(['trackSiteSearch', 'opening hours', 'Support', 0]);
keyword category results
(required) (optional) (optional)
A result count of 0 is the most valuable number in analytics. It is a list of things people asked your site for and did not get.
Custom dimensions attach your own field to a hit, and they come in two scopes that behave differently:
- Visit scope describes the whole session: logged in or not, plan type, customer segment. Set it once and it applies to the visit.
- Action scope describes one pageview or event: article author, product category, form step. Action-scope dimensions can also be extracted automatically from the page URL, the page title, or a URL query parameter, so you often do not need to set them in code at all.
_paq.push(['setCustomDimension', 1, 'member']); // before trackPageView
# an installation ships with 5 slots per scope; add more from the CLI
$ ./console customdimensions:add-custom-dimension --scope=visit --count=10
Now the part that matters more than any of the syntax. Do not track something because you can. Every event you invent is a name you have to keep meaning the same thing for years, across redesigns and across whoever maintains the site after you.
The practical version of that rule is to name events after what happened in the business, not after what was clicked:
fragile trackEvent('Buttons', 'Click', '.green-button')
→ a redesign renames the class and the report dies silently
durable trackEvent('Contact', 'Submit', 'Quote form')
→ survives any redesign, because it describes the outcome
The second one keeps working when the button turns blue, moves to another page, or becomes a link. The first one keeps reporting until somebody changes a stylesheet, and then reports nothing while looking exactly as healthy as before. Analytics that breaks loudly is a nuisance; analytics that breaks silently costs you a year of comparisons.
4.7 Single-Page Applications
The standard tracking code records one pageview when the document loads. A single-page application then changes the URL and the entire contents of the screen without ever loading another document, so Matomo hears nothing more. Everything after the first screen is invisible.
The fix is to tell the tracker that a virtual pageview happened, and the order of the calls is the whole trick:
// on every route change in your application
_paq.push(['setCustomUrl', window.location.href]);
_paq.push(['setDocumentTitle', document.title]);
_paq.push(['trackPageView']);
The two set calls must come first. trackPageView reports whatever the tracker currently believes the URL and title to be, so setting them afterwards labels the next pageview instead of this one. This is section 4.2's queue rule again, and single-page applications are where it does the most damage: every pageview is attributed to the previous screen, the reports look populated, and the numbers are quietly wrong rather than obviously missing.
Matomo's fuller example also carries the referrer and clears page-scoped custom variables, which matters when one screen's context must not leak into the next:
_paq.push(['setReferrerUrl', previousUrl]);
_paq.push(['setCustomUrl', currentUrl]);
_paq.push(['setDocumentTitle', 'My New Title']);
_paq.push(['deleteCustomVariables', 'page']);
_paq.push(['trackPageView']);
Two traps specific to this pattern. Double counting: frameworks that fire a route event on the initial load will track the first screen twice, once from the standard snippet and once from your handler, so skip the first route event. And timing: if you track before the framework has updated document.title, every virtual pageview carries the previous screen's title. Track after the render, not during the transition.
If you would rather not maintain this in application code, Matomo Tag Manager can drive it from history-change triggers instead, which is section 5.3.
Back to top5. Moderate Use Cases
5.1 Segments
A segment is a filter applied to the whole reporting interface at once. Instead of reading one report about everybody, you read every report about one group: visitors from Germany, visitors on mobile, visitors who converted a goal, visitors who arrived from a campaign.
Segments are built from the same dimensions the reports use, combined with AND and OR. In the API they appear as a compact string:
countryCode==de;deviceType==smartphone
; means AND
, means OR
== equals != not equals
=@ contains !@ does not contain
=^ starts with =$ ends with
Here is the part that surprises administrators. A segment is not a display filter. Matomo cannot filter a pre-calculated total, so applying a segment means building a whole new set of archives from the raw log data for that segment. That is why a new segment on a large site can take a long time to show anything, and why segments are the usual explanation for a Matomo that suddenly became slow.
Each segment can be set to process in real time or to be pre-processed by the cron job. On a busy site, pre-processed is the right answer, and section 6.1 explains the setting that enforces it.
5.2 Campaign Tracking
Matomo recognises campaign parameters on incoming URLs and attributes the visit to that campaign rather than to the referrer. It understands its own parameters and the utm_ family:
https://example.com/offer?mtm_campaign=spring&mtm_kwd=newsletter
https://example.com/offer?utm_campaign=spring&utm_source=newsletter
Campaign attribution is stored on the visit and, with cookies enabled, remembered across later visits, so a conversion three days later can still be credited to the newsletter that started it. Turn cookies off and that memory goes with them, which section 5.6 covers.
5.3 Matomo Tag Manager
Matomo ships with its own tag manager, included at no extra cost since June 2018. It follows the same model as other tag managers, with three moving parts:
- Tags are snippets that run on your site: the Matomo tracker itself, a conversion pixel, an embedded widget.
- Triggers decide when a tag fires: on a pageview, on a click, on a form submission, after a scroll depth.
- Variables supply values to tags and triggers: the page URL, the clicked element, a data layer value you set yourself.
These live in a container, which is the single script you place on the site. When Matomo Tag Manager is installed for the first time it creates a default container already holding the Matomo tracking tag, a pageview trigger, and the Matomo configuration variable, so a fresh container tracks pageviews without you configuring anything.
Use the Preview / Debug mode before publishing. It shows, for each action you take on the site, which triggers fired, which tags ran, and what every variable evaluated to. Changes are not live until you publish a version, which also means you can roll back to a previous version when a release breaks tracking.
5.4 The Privacy Settings That Actually Matter
These sit under Administration > Privacy > Anonymize data, and they are the settings people assume are already on.
| Setting | What it does | Sensible value |
|---|---|---|
| Anonymize visitors' IP addresses | Masks the last bytes of the address in memory, before it is written, so the full address is never stored | On, 2 bytes masked; 3 bytes where you want more distance |
| Use masked IP for geolocation | Looks up the country from the masked address instead of the real one | Yes: higher privacy, coarser location |
| Replace User ID with a pseudonym | Stores a hash instead of the identifier you passed in | On, whenever you set a User ID at all |
| Anonymize Order ID | Prevents storing the ecommerce order reference, which usually maps to a named customer in another system | On, unless you have a stated reason |
| Regularly delete old raw data | Removes log rows older than a chosen age | On; Matomo recommends keeping detailed logs for 3 to 6 months |
| Support Do Not Track | Honours the browser's DNT header |
Deprecated; do not rely on it, see below |
The IP setting deserves the emphasis it gets in the table. The masking happens in memory, on arrival, before the row is written. The full address is not stored and then cleaned up later. It is never stored at all, which is a materially different claim and the one that matters in a privacy statement. It is also a claim you can verify in one SQL query rather than take on trust, which section 7.1 does.
One risk is not on that screen at all, and it undoes every setting that is. Your own URLs and page titles can carry personal data straight into the database. A path like /account/peter-martin/orders, a title like "Invoice for J. de Vries", or a query string carrying an email address after a form post are all recorded verbatim, and no amount of IP anonymisation touches them.
Four ways to deal with it, in order of preference:
- Do not put it in the URL. The cheapest fix is upstream of Matomo entirely.
- Exclude the query parameters that carry it, per site, so Matomo strips them before storing.
- Override what gets reported with
setCustomUrlandsetDocumentTitle, sending a generic/account/ordersinstead of the real path. - Mask content in the interface with the
data-matomo-maskHTML attribute, which keeps personal content out of features that reproduce the page.
Matomo does defend you against the worst of it automatically: fields matching patterns such as credit card numbers, phone numbers, email addresses, passwords and social security numbers are always anonymised and never recorded. Treat that as a safety net rather than a strategy, because it cannot recognise that peter-martin in a path is a person's name.
This is the general lesson worth carrying out of the whole privacy section: privacy starts in the measurement design, not in the settings screen. Deciding not to collect something is stronger than collecting it and configuring protection around it afterwards, and it is the only approach that cannot be undone by a future misconfiguration.
Do Not Track is a genuine curiosity: a privacy feature that stopped being useful. The W3C working group that produced the standard was closed, browsers began sending the header without asking the user, Safari removed support entirely, and a signal that nobody sets deliberately means nothing. Matomo has deprecated it and says it may be removed in a future major release. Use the opt-out mechanism instead, which is an actual choice by an actual person.
5.5 The Opt-Out, and the Cookie Paradox
Sections 5.4 and 7.4 both send you to "the opt-out mechanism". It deserves an explanation, because it is the one privacy control your visitors actually operate, and because the way it works contains a genuine contradiction that nobody has solved.
You place an opt-out form on your site, normally on the privacy page. Matomo generates the code for you in Administration > Privacy > Users opt-out, and offers two versions that behave differently in the one situation that matters:
| Version | How it works | Fails when |
|---|---|---|
| Tracker-based | Loads JavaScript from your Matomo instance, which builds the form inside a <div> you place |
A blocker stops requests to your Matomo domain: the form never appears |
| Self-contained | Carries all the JavaScript inline, shows the form and sets the cookie itself | Rarely; nothing external has to load |
Prefer the self-contained version, and notice why. The tracker-based form is fetched from the same host as the tracker, so the visitor most likely to have a content blocker installed is exactly the visitor whose opt-out form silently fails to render. The privacy control is missing for the people who most wanted it.
Now the mechanism. Opting out sets a cookie, mtm_consent_removed, which Matomo describes as containing no personal data and which simply tells the tracker not to collect anything about this browser. Its default lifetime is 30 years, which is a polite way of saying "for ever, as far as a browser is concerned".
Read that again, because it is the paradox: to remember that somebody does not want to be tracked, you have to store something on their device.
the visitor opts out → mtm_consent_removed is set
the visitor clears cookies → the opt-out is gone, tracking resumes
the visitor blocks all
cookies → the opt-out cannot be remembered at all
There is no way around this, in any analytics tool, and it is worth understanding rather than resenting. A stateless request carries no memory of a previous decision. The only alternative would be to identify the person in order to look up their preference, which is precisely what they asked you not to do. Storing a single flag that says "nothing about this browser" is the least invasive option available, and it is still a stored flag.
Two consequences follow that people get wrong.
The opt-out is per domain, because a cookie is per domain. A visitor who opts out on example.com is not opted out on example.org, even when one Matomo installation tracks both. If you run analytics for several sites, each site needs its own opt-out form, and a visitor has to use each one.
An opt-out is not consent. This is the distinction that matters legally and the one the naming hides. An opt-out assumes permission and lets people withdraw it. Consent assumes nothing until somebody agrees. They are opposite defaults, and Matomo has a separate API for the second:
OPT-OUT (tracking is on; the visitor can switch it off)
_paq.push(['optUserOut']); stop tracking this browser
_paq.push(['forgetUserOptOut']); undo it
_paq.push(['isUserOptedOut']); returns true or false
CONSENT (nothing is tracked until the visitor agrees)
_paq.push(['requireConsent']); track NOTHING until consent
_paq.push(['setConsentGiven']); consent for this session only
_paq.push(['rememberConsentGiven']); consent, remembered in a cookie
_paq.push(['forgetConsentGiven']); withdraw it
COOKIE CONSENT (track, but set no cookie until the visitor agrees)
_paq.push(['requireCookieConsent']);
_paq.push(['setCookieConsentGiven']);
_paq.push(['disableCookies']); no first-party cookies at all;
existing ones are deleted on the
next pageview
The middle block is the one to use where consent is legally required, and requireConsent must be pushed before trackPageView, for the queue-order reason in section 4.2. Get that order wrong and the pageview you were trying to withhold has already been sent.
Note the deliberate gap between setConsentGiven and rememberConsentGiven: the first is one-time, so the visitor is asked again in their next browser session, while the second stores a cookie so they are not. Both are defensible; only one of them is what most cookie banners actually promise.
The third block exists for a case worth knowing: tracking that runs without any cookie at all. That is what a consent-exempt configuration uses, and it is why section 7.4's compliance mode can leave measurement running while asking nobody for anything.
5.6 Cookies, and What Turning Them Off Costs
The default tracker sets first-party cookies. The important ones:
| Cookie | Purpose | Expiry |
|---|---|---|
_pk_id |
The visitor ID: how a returning visitor is recognised | 13 months |
_pk_ses |
Short-lived session cookie for the current visit | 30 minutes |
_pk_ref |
Attribution: the referrer that brought the visitor in | 6 months |
_pk_cvar |
Custom variables for the current visit | 30 minutes |
mtm_cookie_consent |
Records that the visitor consented | 30 years by default |
mtm_consent_removed |
Records that the visitor opted out | 30 years by default |
You can run without any of them, and Matomo will still count visits. What you lose is precise, and worth reading before you decide:
- Unique visitors, new visitors and returning visitors become unreliable, because recognising the same person on a later day is exactly what the cookie was for.
- Goal and ecommerce conversions are attributed to the channel of the visit that converted, not to the channel that originally brought the person in. Your newsletter stops getting credit for the sale it started.
- Days since last visit, visits by visit count, visits to conversion and days to conversion all lose accuracy.
- Multi Attribution and Cohort reports stop producing usable data at all.
Notice the pattern: everything that survives is about a single visit, and everything that breaks is about linking visits together over time. That is not a coincidence. It is the same capability described from two directions, and it is the thing consent is actually about.
5.7 Data Retention
Section 1.4 established that Matomo stores your analytics twice. Retention is configured twice as well, and the two settings do different jobs:
Regularly delete old RAW data log_visit, log_link_visit_action, ...
→ frees most of the disk space
→ keeps every existing report intact
→ but you can no longer: open the Visits Log for that period,
build a NEW segment covering it, or replay heatmaps and recordings
Regularly delete old REPORT data archive_numeric_*, archive_blob_*
→ frees comparatively little space
→ the numbers themselves disappear from the dashboard
Almost always you want the first and not the second. Raw data is where the growth is and where the personal data is; aggregated reports are small and are the whole reason you keep history. Matomo's own guidance is to keep detailed logs for three to six months.
Set this up on day one rather than in year three. Deleting three years of accumulated raw rows in one operation on a live database is a very different job from never having let them accumulate.
5.8 Importing from Google Analytics
Matomo can import historical Google Analytics data through the Google Analytics Importer plugin, available by default on Cloud and installable from the Marketplace On-Premise. Two constraints shape how you plan a migration:
- The import creates a new site in Matomo. It cannot be merged into an existing Matomo site, so your imported history and your new tracking live side by side rather than in one continuous line.
- Google's API has a daily request limit. Matomo notes the import typically reaches that limit after roughly four months of data, so a multi-year import runs over several days.
Start the import in parallel with running both tools for a few weeks. Overlapping data is the only way to learn how your two sets of numbers differ before you have to explain the difference to somebody.
Back to top6. Advanced Use Cases
6.1 Archiving: The One Thing That Decides Whether Matomo Is Fast
If you run Matomo On-Premise and read only one section of this article, read this one. More self-hosted Matomo installations are ruined by this than by every other cause combined.
Reports come from the archive tables, and something has to build them. Out of the box, that something is the browser: when you open a report Matomo has not archived yet, it archives it there and then, while you wait.
On a small site this is invisible. On a busy site it is a spiral. The report takes twenty seconds, the user reloads, that starts a second archiving run, the database slows further, and every additional click makes it worse.
The fix is to move archiving to a scheduled job and forbid the browser from doing it:
# crontab, running as the same user as your web server
5 * * * * www-data /usr/bin/php /path/to/matomo/console core:archive \
--url=https://analytics.example.com/ > /var/log/matomo-archive.log
That console is worth knowing beyond this one command. It is Matomo's command-line interface, built on Symfony Console, and it handles archiving, cache clearing, diagnostics, plugin activation, scheduled tasks, updates and database maintenance. Plugins can add their own commands to it, and core:archive is treated as a stable public interface whose name and parameters do not change between versions, which is what makes it safe to put in a crontab and forget.
Then, in Administration > System > General settings:
Archive reports when viewed from the browser : No
Archive reports at most every X seconds : 3600
And in config/config.ini.php, the setting that closes the remaining gap:
[General]
browser_archiving_disabled_enforce = 1
That last line matters more than it looks. Turning off browser archiving in the interface still lets segments be archived in real time on demand, and segments are the most expensive archiving there is. This setting stops that too.
It also has a consequence you must know before you set it, because otherwise you will file a bug against yourself: with enforcement on, any segment configured to process in real time will never show data outside the live reports. Matomo therefore creates new segments as pre-processed when the setting is enabled. Existing segments created before you turned it on are the ones to check, and they are the usual cause of "my segment reports went empty after I fixed the performance problem".
A self-hosted Matomo that feels slow is almost never short of CPU. It is being asked to calculate reports at the moment somebody looks at them, which is the one moment it should be doing nothing but reading.
6.2 The Reporting API
Every report you can see is also an API call, and the API is not an add-on: the interface is built on it. That means anything on screen can be fetched, scheduled, or joined to other data.
Requests go to your Matomo root with module=API:
$ curl -s "https://analytics.example.com/index.php" \
-d "module=API" \
-d "method=VisitsSummary.get" \
-d "idSite=1" \
-d "period=month" \
-d "date=2026-08-01" \
-d "format=json" \
-d "token_auth=YOUR_TOKEN"
{"nb_uniq_visitors":4821,"nb_visits":6390,"nb_actions":18244,
"nb_visits_converted":143,"bounce_count":2871,"sum_visit_length":902144,
"max_actions":57,"bounce_rate":"45%","nb_actions_per_visit":2.9,
"avg_time_on_site":141}
Send the token in the POST body, as above, not in the query string. A token in a URL ends up in your access logs, in proxy logs, and in browser history, and a Matomo token is not a read-only key by default.
The standard parameters:
| Parameter | Meaning |
|---|---|
method |
Module.action, for example Actions.getPageUrls or Referrers.getWebsites |
idSite |
Site ID, a comma-separated list, or all |
period |
day, week, month, year, or range |
date |
YYYY-MM-DD, a range with a comma, or a keyword such as today, yesterday, last30 |
format |
json, xml, csv, tsv, html, rss, original |
filter_limit |
Rows returned. Defaults to 100; use -1 for all rows |
filter_offset |
Row to start at, for paging |
segment |
A segment string, as in section 5.1 |
flat |
1 flattens nested tables, so page URLs come back as full paths instead of a folder tree |
expanded |
1 includes subtables inline |
That default of 100 rows is the most common cause of a wrong number in a Matomo-based dashboard. Somebody fetches page URLs, sums the visits column, compares it to the total on the dashboard, and finds it short. The API answered exactly what was asked: the top hundred rows. Set filter_limit deliberately in every script you write.
One more habit worth having: method=API.getReportMetadata returns the list of every report available on your installation, including the ones added by plugins. It saves guessing at method names.
6.3 The Tracking HTTP API
JavaScript is one way to send data to Matomo, not the only one. Underneath, tracking is a plain HTTP request to matomo.php, and anything that can make an HTTP request can track.
https://analytics.example.com/matomo.php
?idsite=1 # required: which site
&rec=1 # required: literally "record this"
&url=https://example.com/order/complete
&action_name=Order%20complete
&_id=af344a398df83874 # visitor id, 16 hex chars
&send_image=0 # reply 204 instead of a 1x1 GIF
By default the endpoint answers with a 1x1 transparent GIF, which is what makes image-only tracking possible in a plain HTML email or a page with no JavaScript. Adding send_image=0 returns an HTTP 204 instead, which is faster and is what you want from server-side code.
The same endpoint accepts bulk tracking: one POST carrying many hits as a JSON body, which is what you want when a backend job has a thousand actions to record and you do not want a thousand HTTP requests.
$ curl -i -X POST --data '{
"requests": [
"?idsite=1&rec=1&url=https://example.com/a&action_name=Page A",
"?idsite=1&rec=1&url=https://example.com/b&action_name=Page B"
],
"token_auth": "YOUR_TOKEN"
}' https://analytics.example.com/matomo.php
Each array entry is exactly the query string you would otherwise have sent on its own, and every value inside it must be URL-encoded. The token_auth is needed for the cases that require authentication, such as setting a custom timestamp on historical data.
Server-side tracking is the honest answer to blocked scripts. It also moves a responsibility onto you: the browser is no longer supplying the address, the user agent, or the referrer, so your code must send them, and consent must be checked before your code decides to track rather than in the browser after the fact.
6.4 Log Analytics: Analytics Without a Tracker
This is Matomo's most underused capability. It can build its reports from your web server's access logs instead of from JavaScript, using an importer script shipped with it.
$ python3 /path/to/matomo/misc/log-analytics/import_logs.py \
--url=https://analytics.example.com \
--idsite=1 \
--recorders=4 \
--enable-static \
--enable-bots \
--enable-http-errors \
--enable-http-redirects \
/var/log/apache2/access.log
The importer recognises the default formats of Apache, Nginx, IIS and Tomcat, plus NCSA Common, Extended and W3C Extended logs, Nginx JSON, and the log formats of several cloud services including AWS CloudFront, S3 and ELB. Formats it cannot detect, such as OVH, are selected explicitly with --log-format-name.
Four things follow from tracking this way, and each one is a real trade:
- No script, no cookie, nothing on the page. Ad blockers and JavaScript-disabled browsers become invisible as a category, because there is nothing for them to block.
- Bots become visible. With
--enable-botsyou see crawler traffic that JavaScript tracking never sees, because crawlers do not run your tracker. - It works backwards in time. Logs you already have can be imported today. This is the only way to get analytics for a period before you installed the tool.
- Less detail per visit. A log line has no screen size, no viewport, no click events, and no page timings, because the server never learned them.
The two methods are not exclusive. Running the tracker for behaviour and importing logs for crawler traffic and for the pre-installation past is a perfectly reasonable setup, as long as you keep them in separate site IDs so the visit counts do not double.
6.5 The Database
Because the data is yours, SQL is available, and some questions are far easier there than through any report. The tables carry your configured prefix, matomo_ by default.
| Table | One row per |
|---|---|
log_visit |
Visit: visitor ID, site, referrer, device, location, config_id |
log_link_visit_action |
Action within a visit: which action, when, time spent |
log_action |
Distinct action name: a URL, a page title, an event name, an SKU, stored once and referenced by ID |
log_conversion |
Goal conversion, tied to a visit |
log_conversion_item |
Line item in an ecommerce order |
archive_numeric_YYYY_MM |
One pre-calculated metric, per site, period and segment |
archive_blob_YYYY_MM |
One pre-calculated report table, compressed |
The log_action design is worth a moment, because it explains why Matomo's tables are smaller than you expect. URLs and titles are not repeated on every row. Each distinct string is stored once in log_action and referenced by an integer everywhere else. A million pageviews of the same page store that URL once.
Two rules for querying directly. Read, never write. Matomo has no idea you changed anything, and the archives will not agree with the logs afterwards. And be careful with counting on log tables: an unindexed COUNT(*) across log_link_visit_action on a busy site will happily block the tracker that is trying to insert into it.
The disciplined way to make a hand-written report available in the interface is a custom plugin that adds an API method and its own archiver, so the result is cached like every other report rather than recalculated on every page load.
6.6 Many Client Sites: One Installation or Several?
If you report to clients, this is the architecture decision you make once and live with. Matomo tracks an unlimited number of sites per On-Premise installation, and up to 30 per Matomo Cloud instance, so the technical ceiling is rarely what decides it.
Start with where the load actually falls, because the intuition is wrong. Site count barely affects tracking and dominates archiving.
| Operation | Scales with | Effect of adding sites |
|---|---|---|
| Tracking a hit | Total hits | Almost none. One insert, keyed by idsite. Fifty small sites cost what one site with the same total traffic costs |
| Archiving (cron) | Sites x periods x segments | Linear and unforgiving. This is the real cost of a site count |
| One site's dashboard | Nothing much | None. It reads that site's pre-built archives; the other 199 sites are irrelevant |
| All Websites dashboard | Number of sites | Direct. It needs a row per site, so this is the page that slows down first |
That table answers your two performance questions. Registering many sites costs you almost nothing at tracking time. Opening one client's dashboard is fast no matter how many clients you have, because archives are per site. The page that degrades is the cross-site overview, and it degrades in proportion to how many sites you added.
When the archiving run stops finishing between crons, split it rather than buying a bigger server:
core:archive --max-websites-to-process=5
core:archive --max-archives-to-process=100
Whatever is not processed in one run is picked up by the next, so several small runs replace one long one and peak memory drops accordingly.
Now the real question, which is not really about performance:
| One installation, many sites | One installation per client | |
|---|---|---|
| Maintenance | One upgrade, one cron, one backup | Multiplied by the number of clients |
| Cross-client overview | All Websites dashboard, one login | Not possible |
| Blast radius | All clients. A bad upgrade or a full disk takes everyone down at once | One client |
| Noisy neighbours | One large client's archiving delays every other client's reports | Isolated |
| Privacy settings | Installation-wide. One anonymisation and retention policy for everybody | Per client, as their contract requires |
| Database access | Cannot be given to one client without exposing all of them | Can be handed over entirely |
| Client leaves | Export their site; the rows stay in your database | Hand them the instance and delete your copy |
Two rows there deserve more weight than the maintenance argument that usually wins these decisions.
The privacy settings are installation-wide. IP anonymisation, retention, and the compliance configuration in section 7.4 apply to the whole installation, not per site. One client who needs 180-day retention imposes it on every other client, and one client who wants two years prevents you giving anyone else a shorter policy. If your clients have genuinely different obligations, that single fact can decide the question on its own.
One database holds every client's visitor data. That is a concentration of personal data you are now the controller of, and a breach is not one client's incident, it is all of them simultaneously. It is also why the per-client login you set up so carefully is not isolation: Matomo's user permissions control the interface, not the database underneath.
For most agencies, one installation with per-site user permissions is the right answer, and the maintenance saving is real. Split a client out when they are large enough to distort archiving for everyone else, or when their contract or regulator asks for something the shared installation cannot give them. Those are the two conditions worth watching for; everything else is a preference.
6.7 Serving Matomo Under Each Client's Own Domain
A single Matomo can answer to several hostnames, so stats.example.com and stats.petermartin.nl both reach the same instance. Each client's page then loads its tracker from a subdomain of its own site. This is a good idea, and it is worth being precise about which of its benefits are real.
On-Premise, point a DNS CNAME (or an A record) at the server, serve a certificate covering the name, and then add the hostname to Matomo's configuration, which is the step people forget:
[General]
trusted_hosts[] = "stats.example.com"
trusted_hosts[] = "stats.petermartin.nl"
Matomo rejects requests arriving on a hostname it does not know, so without this the new domain returns an error rather than tracking. On Matomo Cloud the equivalent is a custom domain, which requires an Enterprise plan: you create the CNAME to your *.matomo.cloud address and Matomo Support completes the setup with a Let's Encrypt certificate. If your plan is not Enterprise, this option does not exist, which is worth knowing before you promise it to a client.
What you actually gain:
- The request is same-site. A call from
example.comtostats.example.comis first-party. Browser policies that restrict cross-site requests do not apply to it, and that distinction is getting stronger with every browser release, not weaker. - It is not on a blocklist. Blockers ship lists of known analytics hostnames. A subdomain of the client's own site is not on any list, so requests that would have been dropped now arrive, and the reports get closer to reality.
- It looks like the client's own infrastructure, because it is. For a client who dislikes third-party services in their page source, this is a legitimate and accurate answer.
What you do not gain, and should not claim:
- It does not make the cookies first-party. They already were. Matomo's cookies are written by JavaScript through
document.cookieon the tracked page itself, so they belong to the tracked domain no matter which host the tracker is loaded from. This is the benefit most often claimed for the setup and it is not one. - It does not permanently defeat blockers. A custom hostname beats hostname matching. It does not beat a rule that matches the path
/matomo.php, the filenamematomo.js, or the shape of the request. Renaming those buys a little more, and now you are maintaining a disguise rather than an analytics setup. - It does not change your obligations. The same data reaches the same database. Making the collection less visible in the page source does not make it less collection, and it never affects what a privacy policy has to say.
The PHP tracker proxy is a heavier variant of the same idea: files placed on the client's own server (matomo.php, piwik.php, proxy.php and matomo-proxy.php) that receive the hit and forward it to your Matomo server-side. It works where DNS is not yours to change, and it costs more than it looks: the client's web server must be able to make outbound HTTPS requests to your instance, every tracked hit now consumes PHP on their server, and you maintain a set of proxy files on every site you manage. Matomo says plainly that it "may not be suitable if your hosting environment restricts outbound HTTPS requests". Prefer the DNS approach; keep the proxy for the cases that force it.
Back to topServing the tracker from the client's own domain is defensible because it makes measurement more accurate, not because it makes measurement harder to notice. Those two motives produce the same DNS record and very different follow-up decisions, and it is worth knowing which one you are acting on.
7. Something Most Users Do Not Know
7.1 You Can Check the Privacy Claim Yourself
Every analytics tool makes privacy claims. Almost none of them can be verified by the person relying on them. This is the one place where self-hosting changes the kind of statement you are making, and IP anonymisation is the clearest example.
Consider what the same promise looks like from both sides.
Universal Analytics had an IP masking option. Google's own documentation described it precisely, and the description is more revealing than the feature: when the aip=1 parameter was present, "the last octet of the user IP address is set to zero while still in memory", and "at no time is the full IP address written to disk as all truncation happens in memory nearly instantaneously after the request has been received".
Read that carefully. The full address did arrive at Google. It sat in the memory of Google's collection servers, and Google truncated it there. The promise was never "we do not receive your visitors' addresses". It was "we receive them and then discard part of them before writing", which is a completely reasonable engineering design and an entirely unverifiable one. Nothing you could do from outside would tell you whether it happened.
Google Analytics 4 removed the setting altogether. Google's position now is that "in Google Analytics 4, IP masking is not necessary since IP addresses are not logged or stored". That is a stronger privacy claim than Universal Analytics ever made, and it is less checkable than the old one, because there is no longer even a toggle whose effect you might reason about. You are asked to trust a statement about what happens in someone else's memory, on someone else's servers, in a system you cannot read.
Now the same question on a Matomo you host. The addresses are in log_visit.location_ip, stored as binary, so you convert them to read them:
mysql> SELECT INET6_NTOA(location_ip) AS ip, visit_last_action_time
FROM matomo_log_visit
ORDER BY idvisit DESC LIMIT 5;
+---------------+------------------------+
| ip | visit_last_action_time |
+---------------+------------------------+
| 84.105.0.0 | 2026-08-27 09:14:22 |
| 145.53.0.0 | 2026-08-27 09:11:08 |
| 213.127.0.0 | 2026-08-27 09:07:55 |
| 84.105.0.0 | 2026-08-27 09:02:41 |
| 62.140.0.0 | 2026-08-27 08:58:03 |
+---------------+------------------------+
There is no promise involved. The bytes are zero. You did not read a policy, you read the table, and if the bytes were not zero you would know that too.
Two details make the check honest rather than reassuring, and both are worth stating plainly.
The default masks two bytes, not one. An address arriving as 84.105.72.19 is stored as 84.105.0.0, not as 84.105.72.0. You can choose one, two or three bytes in Administration > Privacy > Anonymize data, and you can switch anonymisation off entirely, so "you cannot change it" is not quite the claim to make. Three bytes gives you 84.0.0.0, which is close to useless for geolocation and is the right choice in some jurisdictions anyway.
What you genuinely cannot change is the masking after the fact. This is not a display filter with the real value hidden behind it, and there is no setting that reveals the original. The discarded bytes were never written, so nobody can recover them: not an administrator, not a court order, not the person who owns the server. That is the property worth having, and section 5.4 is where it is configured.
The general shape is worth carrying beyond this one setting. A hosted tool's privacy features are commitments about behaviour you cannot observe. A self-hosted tool's privacy features are states you can inspect. Both can be equally honest, and only one of them lets you find out. It is also the reason the next subsection is interesting: config_id is the rare case where a privacy property is enforced by destroying something rather than by promising not to look at it.
7.2 There Is a Fingerprint, and It Is Deliberately Broken
When a visitor rejects cookies, Matomo still has to decide whether the next request is the same person continuing a visit or somebody new. It solves this with a value called config_id, a hash of the visitor's operating system, browser, browser plugins, IP address and browser language.
That is a device fingerprint. The interesting part is what Matomo does to stop it working like one.
config_id = hash( OS, browser, plugins, IP, language,
site ID,
a random seed that is thrown away every day )
valid for a single visit, at most 24 hours, then rotated
per site the same person browsing two of your sites gets
two different config_ids
the seed discarded each day and CANNOT be recovered
Three design decisions, each removing one capability that a commercial fingerprint exists to provide. The daily seed means the same visitor is a different config_id tomorrow, so returning visitors cannot be recognised. Mixing in the site ID means the value cannot be used to follow anyone between sites. And because the seed is destroyed rather than stored, yesterday's values cannot be recomputed even by the person who owns the server.
That last point is the one worth pausing on. Most privacy claims are promises about what an operator will choose to do. This one is a promise about what an operator can do, enforced by having thrown the key away. Section 7.4 is about how rarely privacy claims are of that kind.
7.3 Your Yearly Unique Visitors Are Missing, and on Cloud You Cannot Turn Them On
Look at a yearly report and the unique visitors figure is absent. This is not a bug, and it is not your data.
Counting unique visitors cannot be done by adding up smaller periods. Twelve monthly figures do not sum to a yearly one, because the same person appears in several months. The only way to compute it is to go back to the raw log data for the entire year and count distinct visitors across all of it, which is an expensive query on a large table.
So Matomo processes unique visitors for day, week and month by default, and skips year and custom date ranges. On-Premise you can turn it on:
[General]
enable_processing_unique_visitors_year = 1
enable_processing_unique_visitors_range = 1
Two conditions apply. The historical reports must be re-processed afterwards, and the raw log data for the period must still exist. If you enabled "regularly delete old raw data" at 180 days, as section 5.7 recommends, then a yearly unique visitor count is arithmetically impossible: the rows it would have counted are gone.
On Matomo Cloud, unique visitors are processed for days and weeks only, and this cannot be enabled for months, years or custom ranges. It is a configuration-file setting, and you do not have the configuration file. This is the clearest example of the Cloud trade-off in section 1.3, and it is worth knowing before someone asks you for a year-on-year unique visitors comparison.
7.4 "Privacy-Friendly" Is a Configuration, Not a Property
This is the most important section in the article, and the one that contradicts the most common reason people choose Matomo.
In October 2021 the French data protection authority, the CNIL, listed Matomo among the analytics solutions that can be configured to qualify for its exemption from collecting consent. Those three words carry the whole meaning. The regulator did not approve a product. It confirmed that the product has the switches, and then published which ones you have to throw.
Since April 2026 that checklist has an assessment screen inside Matomo, at Administration > Privacy > Compliance, available on Cloud and On-Premise from version 5.9.0. Pick a site and Matomo grades every relevant setting as compliant, non-compliant, or unknown, and offers to apply the ones it can enforce automatically.
Read what it applies, because this is the real price of consent-free analytics:
IP addresses masked to two bytes
first-party cookies only
campaign parameters and advertising identifiers stripped at ingestion
segmented data rounded to the nearest ten
data retention set to 180 days
DISABLED:
Visits Log
Visitor Profiles
User ID
heatmaps
session recordings
A/B testing
cross-domain tracking
NOT DONE FOR YOU:
the opt-out mechanism (the CNIL requires it; Matomo cannot place it)
custom events (must stay within the permitted categories)
anything marked Unknown (needs a human to look)
Look at the disabled list. Section 4.3 called the Visits Log the best reason to run Matomo. Compliance mode switches it off. So do the visitor profiles, the User ID, and every behavioural feature people buy the premium bundles for.
The rounding is the subtlest item and the most instructive. Segmented figures are rounded to the nearest ten specifically to stop you singling out an individual: "one visitor from Luxembourg on Tuesday using Firefox" identifies a person, and a number that can only be a multiple of ten cannot narrow to one.
A tool cannot be compliant. A configuration can. Matomo's genuine advantage is that the switches exist, are documented, and can be audited by a regulator. It is not that they are already in the right position when you install it.
Two consequences follow that catch people out.
Ecommerce tracking usually breaks the exemption. Individual purchases, order values and anything that links back to an identifiable customer take you out of pure audience measurement, and consent is required again. The tracking you most want to keep is the tracking most likely to disqualify you.
This is French law, not European law. The CNIL exemption applies in France. Other supervisory authorities have their own positions on when analytics needs consent, and national implementations of the ePrivacy directive differ. The CNIL list is the most concrete public specification of consent-free analytics that exists, which makes it an excellent engineering checklist and a bad substitute for advice about your own jurisdiction.
7.5 Self-Hosting Moves the Legal Weight onto You
People choose Matomo On-Premise to stop sending visitor data to a third party. That works, and it has a consequence the marketing does not lead with: you become the sole data controller and the sole processor. Nobody else is holding this data, so nobody else's security team is protecting it, nobody else's retention policy is trimming it, and nobody else's compliance department is watching the regulations change.
Concretely, self-hosting makes each of these your job:
- Keeping Matomo patched. It is a public-facing PHP application with a database full of personal data, which is a description of a target.
- Not leaving the installation open. A default Matomo answers to anyone who finds it; the reporting interface, the API, and the tracker all live under the same host.
- Backups that you have actually restored, of a database that grows faster than the rest of the site combined.
- Answering a subject access or erasure request, which now means running the deletion yourself in Administration > Privacy > GDPR Tools.
- Deciding retention, because nothing external will ever force the question.
This is not an argument against self-hosting. It is an argument for making the choice with both columns visible. "We host it ourselves so the data is safe" is only true when somebody is doing the work that makes it true.
7.6 You Can Have Analytics for Last Year, Starting Today
Every tracker-based analytics tool begins on the day you install it. Ask what happened last spring and the answer is that nobody was watching.
Matomo's log importer breaks that rule, because your web server was watching. Access logs are written whether or not any analytics exists, and section 6.4's importer will read them. If your host keeps twelve months of logs, you can have twelve months of Matomo reports the day you install it.
This is genuinely valuable in two situations that come up often: reconstructing what a site was doing before a redesign that nobody measured, and investigating an incident after the fact, where crawler and error traffic is exactly what you need and exactly what JavaScript tracking never recorded.
Check your log rotation before you count on it. The usual reason this does not work is that logs were rotated away weeks ago, which is worth knowing now rather than during an incident.
7.7 Deleting Raw Data Silently Disables Features
Section 5.7 recommended deleting old raw data, and section 7.3 has already shown one thing that stops working. It is worth listing the rest in one place, because the failure is quiet: nothing errors, features simply return nothing for older periods.
after raw log data is deleted, for those dates:
Visits Log and Visitor Profile empty
NEW segments cannot be built at all
heatmaps and session recordings permanently gone (they are raw data,
not aggregated reports)
unique visitors for year / range cannot be computed
existing reports and dashboards completely unaffected
The trap is the third line. Heatmaps and session recordings are premium features people pay for, and they are not stored as archived reports. They live in the raw data, so a retention policy set for privacy reasons deletes the recordings too. That is correct behaviour, and it surprises everyone the first time.
The second line is the one to plan around: existing segments keep working, because their archives were already built. Segments you invent next year cannot reach back past your retention window. If a segment is going to matter, create it before the data ages out, even if nobody is looking at it yet.
7.8 Knowing Where Matomo Stops
Good tools have edges. These are the ones worth knowing:
- It stops at your site. Matomo measures visitors who arrive. It knows nothing about how search engines crawl or index you, what you rank for, or what was seen but not clicked. That is Search Console's data and it comes from a different source entirely.
- It stops at what reaches the tracker. A blocked script is an untracked visit. Log import (section 6.4) is the only way to see traffic that never ran your JavaScript.
- It stops at whichever privacy settings you chose. Every anonymisation in section 5.4 is deliberate loss of detail. The masked byte is not recoverable later, by anyone, including you.
- It stops at correlation. Analytics tells you what happened, never why. A page with a high bounce rate might be bad, or might be answering the question so completely that nobody needs a second page.
- It stops at the browser's honesty. User agents are freely settable, referrers can be stripped by the sending site, and privacy-focused browsers alter or remove both. Some proportion of every report is guesswork about identity, and no analytics tool anywhere avoids that.
8. Best Practices
- Set up the archiving cron before you need it. Add the cron job, set browser archiving to No, and set
browser_archiving_disabled_enforce = 1. This is the single highest-value action on a self-hosted Matomo, and it is much easier to do on a quiet site than on a struggling one. - Configure retention on day one. Delete old raw data, keep aggregated reports. Three to six months of raw data is Matomo's own recommendation.
- Decide about yearly unique visitors before you set retention, not after. The two settings contradict each other, and only one order lets you have both.
- Turn on IP anonymisation immediately. It masks before writing, so it protects nothing retroactively. Every day it is off produces rows you cannot fix later.
- Run the Compliance assessment and read the results, even outside France. Treat it as the most concrete available specification of what consent-free analytics costs, then decide deliberately which items you are not going to accept.
- Name events after outcomes, not after elements.
trackEvent('Contact', 'Submit', 'Quote form')survives a redesign; an event named after a CSS class dies silently the day somebody edits a stylesheet. - Keep personal data out of URLs and page titles before you configure anything else. IP anonymisation does nothing about a customer's name in a path, and the automatic pattern-matching cannot recognise one.
- In a single-page application, set the URL and title before
trackPageView, and skip the framework's initial route event. Getting this wrong produces populated reports with every pageview attributed to the previous screen. - Use the self-contained opt-out code, not the tracker-based form. The tracker-based one loads from your Matomo host, so a content blocker hides the privacy control from precisely the visitor who came looking for it.
- Decide one installation or several on the privacy settings, not the maintenance effort. Anonymisation and retention are installation-wide, so clients with different obligations cannot share one instance however convenient that would be.
- Add every hostname to
trusted_hosts[]before pointing a client's subdomain at your Matomo. Matomo rejects hostnames it does not know, and the failure looks like broken tracking rather than a configuration error. - Budget premium plugins as subscriptions. The free core is genuinely complete analytics; heatmaps, funnels, A/B testing and SAML are not in it, and their annual price is a business number.
- Never put a token in a URL. Send
token_authin the POST body. Tokens in query strings end up in access logs and browser history. - Set
filter_limitin every API call. The default of 100 rows will silently give you a wrong total the first time you sum a column. - Use Preview / Debug before publishing a tag manager version, and remember you can roll back a container version when a release breaks tracking.
- Push configuration calls before
trackPageView, not after. Order in the_paqqueue is the cause of most custom tracking that "does not work". - Keep log import and JavaScript tracking in separate site IDs so the same visit is never counted twice.
- Query the database read-only. Writing to log tables desynchronises them from the archives, and heavy counts on
log_link_visit_actioncan block the tracker. - Go to the source. User documentation is at matomo.org/guide, the FAQ at matomo.org/faq, and developer documentation at developer.matomo.org. There is a live installation to explore at demo.matomo.org. Settings and defaults change between versions; this article was checked against those sources in August 2026, against Matomo 5.13.0.
9. Common Mistakes
9.1 Ten Myths
| Myth | Reality |
|---|---|
| "Matomo is GDPR compliant, so I do not have to do anything" | Matomo can be configured to meet a regulator's conditions. Out of the box it sets cookies, records a Visits Log, and keeps raw data. The switches exist; throwing them is your job. |
| "Self-hosting means the data is safe" | Self-hosting means nobody else is responsible for it. Patching, access control, backups and erasure requests all move to you. |
| "I do not need a cookie banner because I use Matomo" | Only if you have configured the exemption, in a jurisdiction that grants one. Default Matomo sets first-party cookies and stores per-visitor detail. |
| "Matomo is slow" | Almost always this is browser-triggered archiving, or a real-time segment on a large site. See section 6.1. |
| "My numbers are lower than Google Analytics, so tracking is broken" | Different visit definitions, different bot filtering, different handling of blocked requests. Two tools never agree, and the gap is not evidence of a fault in either. |
| "Deleting old data will destroy my history" | Deleting old raw data leaves every existing report intact. Deleting old report data is the one that removes your history, and it is a separate setting. |
| "Unique visitors are missing, something is broken" | They are not processed for year or custom ranges by default, because the calculation needs the raw data for the whole period. On Cloud, months and years cannot be enabled at all. |
| "Cookieless tracking is free" | It costs you returning-visitor recognition, multi-visit attribution, cohorts, and several conversion reports. Section 5.6 lists them. |
| "The API gave me a different total than the dashboard" | filter_limit defaults to 100. You summed the top hundred rows. |
| "My analytics has no personal data because I anonymise IP addresses" | Your own URLs and page titles are stored verbatim. A path containing a customer name is personal data that no privacy setting in Matomo will touch. |
| "My single-page app is tracked, the reports are full" | Full reports are not correct reports. Set the URL and title after trackPageView instead of before and every screen is logged under the previous screen's name, which looks entirely healthy. |
| "Matomo is free" | The core is, for unlimited sites and hits. Heatmaps, session recordings, funnels, form analytics, cohorts and A/B testing are commercial plugins on annual subscriptions, and one of them can cost more per year than the server. |
| "Serving the tracker from my own subdomain makes the cookies first-party" | They already were. Matomo's cookies are set by JavaScript via document.cookie on the tracked page, whatever host the tracker loads from. The real gains are same-site requests and not being on a blocklist. |
| "Opting out means nothing is stored on the visitor's device" | The opt-out is something stored on their device: a cookie saying not to track this browser. Clear cookies and the opt-out disappears with them. |
| "Adding more client sites will slow down the tracking" | Tracking load follows hits, not site count. Archiving is what scales with the number of sites, and the All Websites dashboard is the page that feels it first. |
| "config_id is a fingerprint, so Matomo tracks people like everyone else" | It is built from fingerprint-like inputs and then deliberately crippled: rotated daily, scoped per site, and seeded with a random value that is destroyed each day and cannot be recovered. |
9.2 Other Traps to Avoid
- Excluding your own IP address and then forgetting. Addresses change. You end up excluding a stranger and tracking yourself, and the report looks entirely normal while it happens.
- Letting the database grow with no retention policy until the raw tables are larger than everything else on the server, and the first deletion has to run on a live, struggling system.
- Changing
visit_standard_lengthand then comparing the new numbers with the old ones. Every visit-derived metric shifts, and nothing about your visitors changed. - Enabling
browser_archiving_disabled_enforcewithout checking existing segments. Segments still set to real-time processing go permanently empty outside the live reports. - Assuming heatmaps and session recordings survive a retention policy. They are raw data. A privacy-motivated deletion takes them with it.
- Putting
token_authin a URL and then wondering why it appears in an access log you shared with a support forum. - Creating a segment on a large site during business hours, which starts archiving from raw data for every period you then click on.
- Pushing
setUserIdorsetCustomDimensionaftertrackPageView, so the value attaches to the next pageview instead of this one. - Importing Google Analytics data expecting it to merge into your existing Matomo site. It creates a new one, always.
- Trusting a bounce rate on a single-page site. One pageview and no second action is a bounce by definition, even from someone who read every word.
- Tracking the first route change in an SPA on top of the pageview the standard snippet already sent, which doubles every entry page.
- Inventing events faster than you can maintain their names. Every event category is a promise to mean the same thing in three years' time.
- Giving each client a login and calling it isolation. Matomo's permissions control the interface. Every client's raw data still sits in one database, and a breach is all of them at once.
- Promising a Cloud client a custom tracking domain without checking the plan. On Matomo Cloud that is an Enterprise feature.
- Pushing
requireConsentaftertrackPageView, which sends the pageview you were trying to withhold. Consent calls belong at the top of the queue. - Assuming one opt-out covers every site you run. It is a cookie, so it is per domain. Each site needs its own form.
- Forgetting the tracker is public.
matomo.phpaccepts requests from anywhere, so anyone who reads your page source knows your site ID and can send you data. Rely on it for measurement, never for anything that must be trustworthy.
10. Summary
- Matomo is open source web analytics you operate yourself, on your own server or on Matomo Cloud. Every strength and every failure mode follows from where the data physically sits.
- It began as Piwik in 2007, a name invented from "kiwi" that meant nothing. It became Matomo in January 2018, a Japanese word meaning "decent", chosen because it was an argument rather than a label.
- Matomo stores your analytics twice: raw log data and pre-calculated archives. Almost every confusing behaviour becomes obvious once you know which of the two you are looking at.
- Archiving is the whole performance story on a self-hosted install. Move it to cron, set browser archiving to No, and set
browser_archiving_disabled_enforce = 1. - The Visits Log is the feature no hosted competitor matches, and it is also the first thing any privacy configuration switches off. Those are the same fact.
- A segment is not a filter over existing totals. It builds a complete new set of archives from raw data, which is why it is slow and why it needs the raw data to still exist.
- Privacy-friendly is a configuration, not a property. The CNIL exemption is a list of things to disable, including the Visits Log, User ID, heatmaps, session recordings and A/B testing. Since Matomo 5.9.0 the checklist is a screen in the product.
- Self-hosting moves the legal responsibility onto you, in full. There is no second party doing the patching, the retention, or the erasure requests.
- Pageviews are the floor, not the ceiling: events, site search and custom dimensions record what people did rather than only where they went. A search with zero results is the most actionable number in the product.
- A single-page application tracks nothing after its first screen unless you send virtual pageviews, and
setCustomUrlplussetDocumentTitlemust come beforetrackPageViewor every screen is filed under the previous one. - Your own URLs and page titles are stored verbatim. Privacy starts in the measurement design, not in the settings screen, because deciding not to collect something is the only protection a future misconfiguration cannot undo.
- A hosted tool's privacy features are commitments about behaviour you cannot observe; a self-hosted tool's are states you can inspect. Anonymised IP addresses sit in
log_visit.location_ipwith the masked bytes at zero, and one query settles it. config_idis a fingerprint that has been deliberately broken: rotated every 24 hours, scoped per site, seeded with a random value discarded daily and unrecoverable afterwards.- Unique visitors are not processed for year or custom ranges by default, because the count needs raw data for the whole period. On Cloud you cannot enable it, because there is no configuration file to edit.
- Site count barely affects tracking and dominates archiving. One client's dashboard is fast regardless of how many other clients exist; the All Websites overview is the page that slows down. Split a long archiving run with
--max-websites-to-processrather than buying a bigger server. - In a shared installation the privacy and retention settings are installation-wide, so every client gets one policy, and one database holds every client's visitor data. Those two facts, not maintenance effort, are what decide one instance or several.
- Serving the tracker from
stats.clientdomain.commakes requests same-site and unlisted by blockers, which improves accuracy. It does not make the cookies first-party, because they already were. - The opt-out is a cookie, which is the paradox: remembering that somebody does not want to be tracked means storing something on their device. Clear cookies and the opt-out is gone. An opt-out is also not consent - Matomo has a separate
requireConsentAPI for that. - The log importer reads your web server's access logs, which means analytics for a period before Matomo was installed, and visibility of bots that never run JavaScript.
- Deleting raw data is nearly always right, and it silently disables the Visits Log, new segments, heatmaps and session recordings for those dates. Existing reports are untouched.
THE TWO KINDS OF DATA (name which one you mean, first)
log data log_visit, log_link_visit_action, log_action,
log_conversion, log_conversion_item
raw, large, powers Visits Log + NEW segments + heatmaps
archive data archive_numeric_YYYY_MM, archive_blob_YYYY_MM
aggregated, small, powers every dashboard number
FIX A SLOW SELF-HOSTED MATOMO (in this order)
crontab 5 * * * * www-data /usr/bin/php /path/to/matomo/console \
core:archive --url=https://analytics.example.com/
UI Administration > System > General settings
Archive reports when viewed from the browser : No
Archive reports at most every X seconds : 3600
config.ini [General]
browser_archiving_disabled_enforce = 1
then check EXISTING segments: real-time ones go empty
TRACKING CODE (order in the queue matters)
_paq.push(['setCustomDimension', 1, 'member']); config FIRST
_paq.push(['trackPageView']); then the pageview
_paq.push(['enableLinkTracking']); downloads + outlinks
BEYOND PAGEVIEWS
trackEvent category, action, name (opt), value (opt)
trackSiteSearch keyword (required), category, resultCount
resultCount 0 = what people asked for and did not get
setCustomDimension id, value -- push BEFORE the pageview/event
scope visit describes the whole session
scope action describes one pageview/event; can be EXTRACTED
automatically from URL, title or query parameter
5 slots/scope ./console customdimensions:add-custom-dimension
--scope=visit --count=10
naming name events after the OUTCOME, never the element
'Contact','Submit','Quote form' survives a redesign
'Buttons','Click','.green-button' dies silently
SINGLE-PAGE APPLICATIONS (order is the whole trick)
_paq.push(['setCustomUrl', currentUrl]); these two
_paq.push(['setDocumentTitle', title]); FIRST
_paq.push(['trackPageView']); then this
fuller form also: setReferrerUrl, deleteCustomVariables 'page'
trap 1 skip the framework's INITIAL route event, or entry
pages are counted twice
trap 2 track after the render, or every screen carries the
previous screen's title
KEEP PERSONAL DATA OUT OF THE URL (no setting fixes this)
risk /account/peter-martin/orders, "Invoice for J. de Vries",
?email=... after a form post -- all stored verbatim
fixes 1. do not put it in the URL at all
2. exclude those query parameters per site
3. setCustomUrl / setDocumentTitle to send a generic value
4. data-matomo-mask attribute to mask it in the interface
automatic card numbers, phone numbers, emails, passwords and SSNs
are always anonymised -- a safety net, not a strategy
REPORTING API
POST to /index.php module=API&method=VisitsSummary.get
idSite=1&period=month&date=2026-08-01&format=json
token_auth in the POST BODY, never in the URL
filter_limit DEFAULT 100 → use -1 for all rows
format json xml csv tsv html rss original
period day week month year range
segment countryCode==de;deviceType==smartphone
; = AND , = OR =@ contains =^ starts with
API.getReportMetadata lists every report this install has
TRACKING HTTP API
/matomo.php?idsite=1&rec=1&url=...&action_name=...
required idsite, rec=1
send_image=0 reply 204 instead of a 1x1 GIF (use server-side)
bulk POST {"requests":["?idsite=1&rec=1&url=...", ...],
"token_auth":"..."} to /matomo.php
each entry is a normal query string, URL-encoded
LOG IMPORT (analytics for the past, and for bots)
python3 misc/log-analytics/import_logs.py --url=... --idsite=1 \
--recorders=4 --enable-static --enable-bots \
--enable-http-errors --enable-http-redirects access.log
formats Apache, Nginx, IIS, Tomcat defaults; NCSA Common;
Extended; W3C Extended; Nginx JSON; AWS CloudFront/S3/ELB
undetected --log-format-name=ovh, incapsula_w3c
MANY CLIENT SITES (what actually scales)
tracking a hit scales with HITS, not with site count
one site dashboard unaffected by how many sites exist
All Websites page scales with SITE COUNT: slows down first
archiving cron sites x periods x segments: the real cost
split it core:archive --max-websites-to-process=5
core:archive --max-archives-to-process=100
limits On-Premise unlimited sites; Cloud up to 30
shared install privacy + retention settings are INSTALLATION-WIDE
one database = every client's data at once
TRACKER ON THE CLIENT'S OWN DOMAIN
On-Premise CNAME + certificate + [General]
trusted_hosts[] = "stats.example.com"
(without it Matomo REJECTS the hostname)
Cloud custom domain = ENTERPRISE plan only
you gain same-site requests, not on any blocklist, accuracy
you do NOT first-party cookies (already were, set via
gain document.cookie on the tracked page), nor immunity
from path-based blocking of /matomo.php + matomo.js
OPT-OUT vs CONSENT (opposite defaults, different APIs)
opt-out tracking is ON, visitor can switch it off
optUserOut / forgetUserOptOut / isUserOptedOut
stored in mtm_consent_removed, 30 years, PER DOMAIN
clear cookies → opt-out gone; block cookies → cannot opt out
consent nothing tracked until the visitor agrees
requireConsent (push BEFORE trackPageView)
setConsentGiven = this session only
rememberConsentGiven = stored in a cookie
cookies requireCookieConsent / setCookieConsentGiven
disableCookies = no first-party cookies at all
form use the SELF-CONTAINED opt-out code: the tracker-based
form loads from your Matomo host, so a blocker hides it
from exactly the visitor who wanted it
PREMIUM PLUGINS (the "free" caveat)
free core unlimited sites, unlimited hits, no sampling
paid plugins A/B Testing, Heatmaps, Session Recording, Funnels,
Form Analytics, Cohorts, Crash Analytics, Roll-Up,
SAML/LDAP - annual, priced by user tier
budget note one plugin's yearly price commonly exceeds the yearly
cost of the server running the whole installation
PRIVACY SETTINGS THAT MATTER Administration > Privacy
anonymise IP 2 bytes; masked IN MEMORY before writing
84.105.72.19 → 84.105.0.0 (1 byte: .72.0,
3 bytes: 84.0.0.0); the bytes are never written,
so nobody can recover them afterwards
VERIFY it yourself SELECT INET6_NTOA(location_ip) FROM matomo_log_visit
ORDER BY idvisit DESC LIMIT 5;
masked IP for geo yes: coarser country data, no stored address
pseudonymise User ID whenever a User ID is set at all
anonymise Order ID it usually maps to a named customer
delete old RAW data Matomo recommends keeping 3-6 months
delete old REPORT data almost never what you want
Do Not Track DEPRECATED: W3C group closed, Safari dropped it
CNIL CONSENT EXEMPTION Administration > Privacy > Compliance (5.9.0+)
enforced IP masked to 2 bytes, first-party cookies only,
campaign parameters stripped, segmented data rounded to 10,
retention 180 days
disabled Visits Log, Visitor Profiles, User ID, heatmaps,
session recordings, A/B testing, cross-domain tracking
manual the opt-out mechanism, custom event categories,
anything graded Unknown
note ecommerce tracking usually takes you back OUT of the
exemption; this is French law, not EU-wide
DEFAULTS WORTH MEMORISING
visit 30 minutes of inactivity [Tracker] visit_standard_length=1800
_pk_id 13 months visitor ID cookie
_pk_ses 30 minutes session cookie
_pk_ref 6 months attribution cookie
config_id 24 hours max, per site, seed destroyed daily
filter_limit 100 rows (API)
unique visitors day/week/month YES, year/range NO
enable_processing_unique_visitors_year = 1
(On-Premise only; needs raw data to still exist)
Verified August 2026 against matomo.org, developer.matomo.org
and Matomo 5.13.0 (released 16 August 2026)
Read this way, Matomo stops being a privacy badge you install and becomes a database you are responsible for. And when a site owner tells you their analytics is finally private because they host it themselves, the useful question is not where the server is: it is whether anyone has opened the retention settings since the day it was installed, because a tool that quietly keeps every visitor's full path through the site for three years is not made private by the address of the machine it runs on.
Back to top

Peter is a Joomla specialist and a Linux admin for fast, secure and scalable websites.






