How Matomo Works
Matomo collects analytics data through a JavaScript tracker (matomo.js) that sends HTTP requests to a PHP receiver (matomo.php). Each pageview or event generates a GET/POST request containing the page URL, referrer, screen resolution, browser plugins, and a visitor ID stored in a first-party cookie (_pk_id).
The data model centers on three entities:
- Visits -- A session identified by a visitor ID cookie (or fingerprint hash in cookieless mode). Default timeout is 30 minutes of inactivity.
- Actions -- Individual pageviews, downloads, outlinks, site searches, and custom events within a visit.
- Conversions -- Goal completions or e-commerce transactions tied to a visit.
Matomo processes raw log data into aggregated archive tables on a configurable schedule. The archiving process pre-computes reports by period (day, week, month, year) and segment, storing them in archive_numeric and archive_blob tables in MySQL/MariaDB.
Visitor Identification
Matomo uses a layered identification system:
- Visitor ID (
_pk_idcookie) -- 16-character hex string, persists for 13 months by default - User ID -- Explicit identifier you set via
_paq.push(['setUserId', 'user@example.com']), persists across devices - Fingerprint -- Hash of IP + User-Agent + browser plugins + language, used only when cookies are disabled
- Order ID -- E-commerce transaction identifier for deduplication
When User ID is set, Matomo links all visits from that user across devices and sessions. Without it, each browser gets an independent visitor ID.
Installing the Tracking Script
Place the tracking code before the closing </body> tag. Replace your-matomo-domain.com and SITE_ID with your values:
<script>
var _paq = window._paq = window._paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u = "https://your-matomo-domain.com/";
_paq.push(['setTrackerUrl', u + 'matomo.php']);
_paq.push(['setSiteId', 'SITE_ID']);
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>
<noscript>
<img referrerpolicy="no-referrer-when-downgrade"
src="https://your-matomo-domain.com/matomo.php?idsite=SITE_ID&rec=1"
style="border:0" alt="" />
</noscript>
The _paq array is a command queue. Commands pushed before the script loads are executed in order once matomo.js initializes. Commands pushed after load execute immediately.
SPA Tracking
For single-page applications, call trackPageView on each route change:
// React Router, Vue Router, etc.
router.afterEach((to) => {
_paq.push(['setCustomUrl', to.fullPath]);
_paq.push(['setDocumentTitle', to.meta.title || document.title]);
_paq.push(['trackPageView']);
});
Matomo Tag Manager
Matomo Tag Manager is a separate container system. Add the container snippet instead of the standard tracker:
<script>
var _mtm = window._mtm = window._mtm || [];
_mtm.push({'mtm.startTime': (new Date().getTime()), 'event': 'mtm.Start'});
(function() {
var d = document, g = d.createElement('script'),
s = d.getElementsByTagName('script')[0];
g.async = true;
g.src = 'https://your-matomo-domain.com/js/container_XXXXXXXX.js';
s.parentNode.insertBefore(g, s);
})();
</script>
Tags, triggers, and variables are configured in the Matomo UI. The container file is a static JS file regenerated on each publish. Unlike Google Tag Manager, the container runs entirely through your Matomo server -- no third-party requests.
Event Tracking and Custom Dimensions
Custom Events
Track events with category, action, name, and optional numeric value:
// _paq.push(['trackEvent', category, action, name, value])
_paq.push(['trackEvent', 'Form', 'Submit', 'Contact Form']);
_paq.push(['trackEvent', 'Video', 'Play', 'Product Demo', 45]);
_paq.push(['trackEvent', 'Download', 'PDF', '/files/whitepaper.pdf']);
Custom Dimensions
Custom dimensions attach metadata to visits (scope: visit) or actions (scope: action). Configure dimensions in Settings > Custom Dimensions first to get an index.
// Visit-scoped dimension (set once per session)
_paq.push(['setCustomDimension', 1, 'premium']);
// Action-scoped dimension (set before each trackPageView/trackEvent)
_paq.push(['setCustomDimension', 2, 'sidebar-cta']);
_paq.push(['trackPageView']);
// Delete a custom dimension value
_paq.push(['deleteCustomDimension', 2]);
Content Tracking
Track visibility and interaction with page elements:
<div data-track-content
data-content-name="Hero Banner"
data-content-piece="Summer Sale"
data-content-target="https://example.com/sale">
<a href="https://example.com/sale">Shop Now</a>
</div>
Matomo automatically tracks impressions and clicks on elements with data-track-content.
Goals
Goals are configured in the Matomo admin UI (Manage > Goals). They trigger on:
- URL pattern match (contains, exact, regex)
- Page title match
- Custom event match
- File download
- External link click
- Manually via
_paq.push(['trackGoal', goalId, revenue])
// Manual goal conversion with revenue
_paq.push(['trackGoal', 1, 49.99]);
E-commerce Tracking
// Set e-commerce view (product page)
_paq.push(['setEcommerceView', 'SKU001', 'Widget Pro', 'Widgets', 29.99]);
_paq.push(['trackPageView']);
// Add items to cart
_paq.push(['addEcommerceItem', 'SKU001', 'Widget Pro', 'Widgets', 29.99, 2]);
_paq.push(['trackEcommerceCartUpdate', 59.98]);
// Track order
_paq.push(['addEcommerceItem', 'SKU001', 'Widget Pro', 'Widgets', 29.99, 2]);
_paq.push(['trackEcommerceOrder', 'ORDER-123', 59.98, 59.98, 4.80, 5.99, 0]);
// trackEcommerceOrder(orderId, grandTotal, subTotal, tax, shipping, discount)
Self-Hosting and Privacy Architecture
Self-Hosted Deployment
Matomo self-hosted requires PHP 7.2.5+ and MySQL 5.5+ (or MariaDB 10.0+). Recommended stack:
Nginx/Apache -> PHP-FPM 8.x -> MySQL 8.x / MariaDB 10.6+
Docker deployment:
services:
matomo:
image: matomo:5
ports:
- "8080:80"
environment:
MATOMO_DATABASE_HOST: db
MATOMO_DATABASE_USERNAME: matomo
MATOMO_DATABASE_PASSWORD: ${MATOMO_DB_PASS}
MATOMO_DATABASE_DBNAME: matomo
volumes:
- matomo_data:/var/www/html
depends_on:
- db
db:
image: mariadb:10.11
environment:
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASS}
MARIADB_DATABASE: matomo
MARIADB_USER: matomo
MARIADB_PASSWORD: ${MATOMO_DB_PASS}
volumes:
- db_data:/var/lib/mysql
volumes:
matomo_data:
db_data:
Archiving
Disable browser-triggered archiving in production and use cron:
# Run every 5 minutes
*/5 * * * * /usr/bin/php /var/www/matomo/console core:archive --url=https://analytics.example.com > /dev/null 2>&1
Set [General] browser_archiving_disabled_enforce = 1 in config.ini.php to block browser-triggered archiving entirely.
GDPR Consent Management
Matomo supports two consent modes:
Consent Required (opt-in):
// No tracking until consent is given
_paq.push(['requireConsent']);
_paq.push(['trackPageView']);
// When user consents:
_paq.push(['setConsentGiven']);
// Revoke consent:
_paq.push(['forgetConsentGiven']);
Cookie Consent Only (cookieless tracking by default):
// Track without cookies until consent is given
_paq.push(['requireCookieConsent']);
_paq.push(['trackPageView']);
// When user accepts cookies:
_paq.push(['setCookieConsentGiven']);
Cookieless Tracking
Run Matomo without any cookies:
_paq.push(['disableCookies']);
_paq.push(['trackPageView']);
In cookieless mode, Matomo uses a hash of IP + User-Agent to identify visitors within a single day. Visitor recognition across days is lost, but no consent banner is required.
IP Anonymization
Configure in config.ini.php or the admin UI:
[Tracker]
; Mask last 2 bytes of IPv4, last 8 bytes of IPv6
ip_address_mask_length = 2
; Or use full anonymization
use_anonymized_ip_for_visit_enrichment = 1
Server-Side Tracking API
Send tracking data server-side via HTTP requests to matomo.php. Useful for backend events, mobile apps, and IoT:
curl "https://analytics.example.com/matomo.php?\
idsite=1&\
rec=1&\
action_name=Order+Completed&\
url=https://example.com/checkout/complete&\
_id=a1b2c3d4e5f6a1b2&\
uid=user@example.com&\
e_c=Purchase&\
e_a=Complete&\
revenue=149.99&\
cdt=$(date -u +%Y-%m-%d%%20%H:%M:%S)&\
token_auth=YOUR_TOKEN"
Key parameters:
| Parameter | Description |
|---|---|
idsite |
Site ID |
rec |
Required, always 1 |
url |
Full page URL |
action_name |
Page title |
_id |
16-char hex visitor ID |
uid |
User ID (cross-device) |
e_c, e_a, e_n, e_v |
Event category, action, name, value |
cdt |
Override datetime (requires token_auth) |
token_auth |
API token for write access |
cip |
Override visitor IP (requires token_auth) |
Bulk Tracking
Send up to 50 requests in a single POST:
curl -X POST "https://analytics.example.com/matomo.php" \
-H "Content-Type: application/json" \
-d '{
"requests": [
"?idsite=1&rec=1&url=https://example.com/page1&action_name=Page+1",
"?idsite=1&rec=1&url=https://example.com/page2&action_name=Page+2"
],
"token_auth": "YOUR_TOKEN"
}'
Reporting API and Data Export
Matomo's Reporting API exposes every report available in the UI. All endpoints follow the same pattern:
https://analytics.example.com/index.php?module=API&method=MODULE.METHOD&idSite=1&period=day&date=today&format=json&token_auth=YOUR_TOKEN
Common API Calls
Live visitors (last 30 minutes):
curl "https://analytics.example.com/index.php?\
module=API&\
method=Live.getCounters&\
idSite=1&\
lastMinutes=30&\
format=json&\
token_auth=YOUR_TOKEN"
Pageview data for a date range:
curl "https://analytics.example.com/index.php?\
module=API&\
method=Actions.getPageUrls&\
idSite=1&\
period=range&\
date=2025-01-01,2025-01-31&\
format=json&\
filter_limit=100&\
token_auth=YOUR_TOKEN"
Goal conversions:
curl "https://analytics.example.com/index.php?\
module=API&\
method=Goals.get&\
idSite=1&\
idGoal=1&\
period=month&\
date=today&\
format=json&\
token_auth=YOUR_TOKEN"
Custom dimensions:
curl "https://analytics.example.com/index.php?\
module=API&\
method=CustomDimensions.getCustomDimension&\
idSite=1&\
idDimension=1&\
period=day&\
date=last7&\
format=json&\
token_auth=YOUR_TOKEN"
Live API
The Live API returns individual visit details in real time:
# Last 10 visits
curl "https://analytics.example.com/index.php?\
module=API&\
method=Live.getLastVisitsDetails&\
idSite=1&\
period=day&\
date=today&\
filter_limit=10&\
format=json&\
token_auth=YOUR_TOKEN"
Response includes full visit actions, referrer data, geolocation, device info, and custom dimensions per visit.
Segmentation in API Requests
Apply segments to any API call:
# Visits from mobile devices in Germany
&segment=deviceType==smartphone;countryCode==de
# Visitors who completed goal 1
&segment=visitConvertedGoalId==1
# Custom dimension value
&segment=dimension1==premium
Export Formats
All API endpoints support: json, xml, csv, tsv, html, rss, original (PHP serialized).
Common Issues
Tracking requests blocked by ad blockers:
Rename matomo.js and matomo.php to custom filenames. In config.ini.php:
[Tracker]
tracker_js_url = "custom-stats.js"
tracker_url = "custom-collect.php"
Then update the tracking code to reference the new filenames.
Archive cron running slowly:
Check matomo_archive_invalidations table for stuck entries. Increase PHP memory limit and set [General] enable_processing_unique_visitors_multiple_sites = 0 if tracking many sites.
Visitor count discrepancies with cookieless mode:
Without cookies, Matomo cannot track visitors across days. The same person visiting Monday and Tuesday counts as two unique visitors. This is expected behavior. If accuracy matters more than cookie-free operation, use requireCookieConsent and track with cookies for consenting users.
MySQL lock wait timeouts during archiving:
Set [General] enable_sql_optimize_queries = 1 and ensure the archive tables have proper indexes. Consider running archiving during low-traffic hours.
User ID not linking visits:
setUserId must be called before trackPageView. If the user logs in mid-session, the current visit updates but previous anonymous visits are not retroactively linked unless [Tracker] enable_userid_overwrites_visitorid = 1 is set.
Platform-Specific Considerations
Matomo Cloud vs Self-Hosted: Matomo Cloud handles infrastructure, updates, and archiving. Self-hosted gives full database access, unlimited users, and no data caps. The Tracking API and Reporting API are identical across both.
Log Analytics: Matomo can import server access logs instead of using JavaScript tracking. Run the log importer:
python /path/to/matomo/misc/log-analytics/import_logs.py \
--url=https://analytics.example.com \
--idsite=1 \
--token-auth=YOUR_TOKEN \
/var/log/nginx/access.log
Plugin Development:
Plugins extend Matomo via PHP classes that hook into the event system. Key hooks: Tracker.newConversionInformation, API.Request.dispatch, Template.jsGlobalVariables. Plugin code lives in /plugins/YourPlugin/ with a plugin.json manifest.
Database Scaling:
For high-traffic installs (10M+ pageviews/month), use MySQL replication with a read replica for the Reporting API and the primary for the Tracker. Configure in config.ini.php:
[database]
host = "primary-db"
[database_reader]
host = "replica-db"