If your consent banner calls setConsentGiven from a callback that runs after Matomo’s tracking snippet, Matomo deletes the visitor’s _pk_id cookie and creates a fresh one on every page view. The page view is built while cookies are still disabled, and building a request with cookies disabled deletes them. The usual workaround, rememberConsentGiven, fixes the ID but leaves a Matomo consent cookie that outlives the banner’s own and keeps tracking after the banner asks again. The fix is to read the stored choice before trackPageView runs. Tested in Chromium against Matomo 5.13.0 and CookieConsent 3.1.0.
We read everything below in the Matomo tracker source at tag 5.13.0 (the latest release, 16 August 2026, GPL-3.0), then ran it in headless Chromium on 14 September 2026. The setup was a local tracking endpoint that logged every request and vanilla-cookieconsent 3.1.0 as the banner. None of it depends on CookieConsent in particular. The same thing happens with any consent tool that reports its answer through an event.
The two consent modes
Matomo’s JavaScript tracker has two switches. They do different things.
requireConsent |
requireCookieConsent |
|
|---|---|---|
| Before consent | No requests, no cookies. Requests wait in an in-memory queue. | Requests are sent without cookies. |
| On grant | setConsentGiven sends the queue, then turns cookies on |
setCookieConsentGiven turns cookies on and sends a ping |
| Remembered by Matomo | rememberConsentGiven sets mtm_consent |
rememberCookieConsentGiven sets mtm_cookie_consent |
| On withdrawal | forgetConsentGiven deletes _pk_* and sets mtm_consent_removed |
forgetCookieConsentGiven deletes _pk_*. Cookieless requests carry on. |
Two details matter later. The queue lives in memory, so a visitor who accepts on the landing page still gets that page view recorded. A visitor who clicks through to a second page without answering loses the first one: our log showed zero requests across both pages. And refusal writes a cookie. forgetConsentGiven stores mtm_consent_removed and asks for 30 years, which Chromium cut to its 400-day maximum. That cookie is how Matomo remembers the opt-out. If you have scripted a check that expects no cookies after a refusal, it will flag this one.
The symptom: a new visitor ID on every page
This is the wiring Matomo’s guide implies: put requireConsent in the snippet, then call setConsentGiven on every page when the banner says yes.
<script>
var _paq = window._paq = window._paq || [];
_paq.push(['requireConsent']);
_paq.push(['trackPageView']);
// setTrackerUrl, setSiteId ...
</script>
<script src="/matomo.js"></script>
<script src="/cookieconsent.umd.js"></script>
<script>
function syncMatomo() {
_paq.push([CookieConsent.acceptedCategory('analytics')
? 'setConsentGiven' : 'forgetConsentGiven']);
}
CookieConsent.run({ /* ... */ onConsent: syncMatomo, onChange: syncMatomo });
</script>
Accept, then load three pages. The _pk_id value went 67e7dca8…, a31c1b61…, 9249f324…. On each reload the page view went out with an empty _id and _idn=1, the new-visitor flag. A ping followed a moment later carrying an ID nobody had seen before.
The cause is in getRequest(). When Matomo builds any tracking request while cookies are disabled, it calls deleteCookies() first:
if (configCookiesDisabled) {
deleteCookies(); // _pk_id, _pk_ses, _pk_cvar, _pk_ref
}
requireConsent disables cookies whenever there is no remembered consent. trackPageView runs as soon as matomo.js loads, before the banner script has read its own cookie. So the returning visitor’s ID is deleted while the page view is being built. When setConsentGiven arrives a few milliseconds later there is no _pk_id left to read, and loadVisitorIdCookie() generates a random one. requireCookieConsent with setCookieConsentGiven in a callback behaves the same way: in our test the ID rotated on every page view there too.
We did not run a Matomo server, so we have not measured what this does to reports. The request shape is clear, though. The first request of every page claims to be a new visitor, and the ID it later sends has never been seen before. Visitor recognition across visits depends on that ID.
Why rememberConsentGiven is not the answer
Matomo’s own Klaro integration guide calls rememberConsentGiven on accept. That does stop the rotation. It sets mtm_consent, so requireConsent finds remembered consent at load time and cookies are never disabled. It also creates a second record of the visitor’s choice, and the two records expire on different schedules.
CookieConsent keeps its choice for 182 days by default. mtm_consent asks for 30 years. We deleted cc_cookie to simulate that expiry and reloaded. The banner came back asking, and on the same load Matomo sent a page view with _pk_id and mtm_consent attached, before any answer. Bumping CookieConsent’s revision gave the same result. That is the mechanism you would use to ask again after adding a tracker. The banner treats the old consent as invalid and Matomo does not.
Once both tools store the choice, they can disagree. Keep one record, and make it the banner’s.
The fix: decide before trackPageView
Load the consent script first, synchronously, and tell Matomo the answer before the page view is queued. CookieConsent’s run() is async, but it reads the stored cookie before its first await. That makes validConsent() and acceptedCategory() usable on the next line.
<script src="/cookieconsent.umd.js"></script>
<script>
var _paq = window._paq = window._paq || [];
function syncMatomo() {
_paq.push([CookieConsent.acceptedCategory('analytics')
? 'setConsentGiven' : 'forgetConsentGiven']);
}
CookieConsent.run({
categories: { necessary: { enabled: true, readOnly: true }, analytics: {} },
onChange: syncMatomo, // later changes on this page
onConsent: syncMatomo, // first answer on this page
// language, guiOptions ...
});
_paq.push(['requireConsent']);
if (CookieConsent.validConsent() && CookieConsent.acceptedCategory('analytics')) {
_paq.push(['setConsentGiven']); // before trackPageView
}
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
_paq.push(['setTrackerUrl', '/matomo.php']);
_paq.push(['setSiteId', '1']);
</script>
<script src="/matomo.js"></script>
With this order the ID stayed the same across a reload and a navigation. The page view carried _id and _idn=0. onConsent fires again on every load, so setConsentGiven is pushed twice. That is harmless: we saw one page view and no extra ping. Withdrawal through onChange still deleted _pk_id and _pk_ses immediately. For requireCookieConsent, swap in setCookieConsentGiven and forgetCookieConsentGiven. The ID stayed stable there too, and cookieless page views carried on after withdrawal, which is what that mode is for.
Two things about ordering. Matomo sorts requireConsent ahead of trackPageView for commands pushed before matomo.js loads, so the order inside _paq is forgiving. The order of the <script> tags is not. If the banner script is async or defer, or it comes from a tag manager, you are back to the callback pattern. Klaro and tarteaucitron can be wired the same way if you can read the stored choice before the snippet runs. We tested only CookieConsent here.
Checking your own site
You need two page loads and the network tab. Filter on matomo.php.
- Accept, then reload. The page-view request should carry a non-empty
_idand_idn=0. An empty_idfollowed by aping=1request means the ID was just regenerated. - Compare the
_pk_id.*cookie value before and after a navigation. It should not change. - In
requireConsentmode, requests sent after consent includeconsent=1. Before an answer there should be no requests at all. - Look for
mtm_consent. If it is there, delete the banner’s own cookie and reload. If a page view goes out while the banner is still asking, Matomo is working from its own record, not the banner’s.
The last check is really about which cookies survive a change of mind. We cover that for other tools in why refusing does not delete cookies, and the CookieConsent callbacks used here are explained in more depth in the CookieConsent and Consent Mode v2 guide. If you would rather see what fires before and after a choice without opening DevTools, CookieInspector’s scanner records requests and cookies on both sides of the banner.