Matomo archiving is eating your CPU and RAM: fixing 'Allowed memory size exhausted' and scaling on-premise

core:archive dies on month and year with a DataTable.php fatal, MySQL CPU pins for no reason, and raising memory_limit never helps. It's almost always one thing: archiving, not tracking, is what makes Matomo heavy. Here's why more RAM doesn't fix it and what to tune instead.

If your core:archive run finishes day and week fine, then dies on month or year with PHP Fatal error: Allowed memory size of 2147483648 bytes exhausted in .../DataTable.php, you've already noticed the part that makes no sense: you raised memory_limit from 512M to 2G to 8G, and each time the same error came back at the new ceiling. Maybe alongside it, MySQL CPU and PHP-FPM memory jumped one morning and stayed pinned even though traffic never changed, with climulti:request processes piling up and never finishing.

These look like several different problems. They're mostly one. Archiving, not tracking, is what makes Matomo heavy, and on a busy or large install it has to be configured on purpose rather than left on defaults. Once you stop treating tracking as the suspect and start treating archiving as the load source, the fixes line up in a sensible order.

TL;DR

Archiving, not tracking, is what makes Matomo heavy, and the memory a run needs scales with cardinality (distinct URLs, page titles, search terms), not with how much RAM you give PHP. That's why raising memory_limit never stops the DataTable.php fatal; it just moves the crash to a higher ceiling. The fix that holds is to cap the archiving row limits like datatable_archiving_maximum_rows_actions and exclude noisy query parameters, after moving archiving to a CLI cron job with browser triggering off. If MySQL CPU stays pinned while traffic is flat, that's a different problem: an invalidation backlog looping, which you clear with core:invalidate-report-data rather than more hardware.

Why "add more memory" keeps failing

The archiver aggregates raw visits into report tables, and it does that aggregation in memory. How much memory it needs scales with cardinality: the count of distinct URLs, page titles, entry and exit pages, search terms, and segments. It does not scale with how much RAM you hand PHP.

That's the whole trap. A site that imported a file directory as tens of thousands of unique URLs, or that tracks query strings full of session IDs, builds an enormous in-memory DataTable for the Pages/Actions report. Give that process 8 GB and it will happily try to use 8 GB. The ceiling was never the constraint. The table was. So the durable fix is to bound the table and cut the cardinality, not to keep lifting the limit and hoping the next run squeaks under it.

If your install grew this way through log imports, the log-import path is usually where the unique-URL explosion got into the database in the first place. Worth tracing before you tune anything.

  1. 1

    Tracking requests

  2. 2

    Raw log tables

  3. 3

    core:archive cron

    if browser archiving is enabledBrowser report requests trigger archiving here too
  4. 4

    In-memory DataTables

    this is where the memory goes

  5. 5

    Archive tables

  6. 6

    UI and API reports

Where archiving memory is spent

The fixes, in order

1. Archive from cron, and turn off browser archiving

If the UI is still generating reports on demand, you have two uncoordinated systems competing for the same CPU. Move all archiving to a scheduled CLI run as your web-server user:

bash
# Run hourly from cron, as the same user that owns your Matomo files
./console core:archive --url=https://your-matomo.example

--url takes the full address Matomo runs at, sub-path and all, so it works on every install. Newer builds also accept --matomo-domain as a shorthand when Matomo sits on a bare domain with no path. If your install lives under something like /matomo, stay with --url, and run ./console help core:archive before you touch an existing cron entry.

Then stop the browser from triggering its own jobs. In config/config.ini.php, under [General]:

ini
[General]
enable_browser_archiving_triggering = 0
browser_archiving_disabled_enforce = 1
archiving_range_force_on_browser_request = 0

The default for enable_browser_archiving_triggering is 1, which is why a fresh install feels fine until traffic grows and report requests start kicking off live archiving. If users or API clients request segmented reports, browser_archiving_disabled_enforce = 1 stops those segment requests from bypassing the browser-archiving disablement. Custom date ranges are different: core:archive does not pre-archive arbitrary ranges, so use archiving_range_force_on_browser_request = 0 only if you deliberately accept that range reports may not be freshly generated on browser request. The same main toggle is exposed in Administration → System → General Settings if you'd rather not edit the file. If you haven't set the cron side up yet, we've written a full walkthrough of Matomo cron archiving.

2. Give the CLI enough memory, once

Set a real memory_limit in your CLI php.ini. That's the CLI SAPI, a different file from the FPM pool that serves the web UI, and editing the FPM one does nothing for archiving. People lose afternoons to this. While a run is in progress, Matomo also enforces a floor:

ini
[General]
; Matomo refuses to archive below this many MB; default is 768
minimum_memory_limit_when_archiving = 768
Are you editing the CLI php.ini?
Run php --ini and php -i | grep memory_limit as the same user and in the same container or host context that cron uses. If those commands show a different file than PHP-FPM, change the CLI file.

Raising the CLI limit is the right move when a run is genuinely close to the edge and finishing. But if every bump just postpones the same DataTable.php fatal to a higher number, stop. That's the symptom from the top of this post, and the next step is the real fix.

3. Cap the archiving table sizes

This is the lever that actually stops the memory exhaustion. First check whether these limits were raised above Matomo's defaults. If they were, put them back. If they are already at the defaults and month or year archives still die, temporarily lower the action-heavy limits enough to get the failing archives through.

SettingMatomo 5 defaultPathological-site trialReport tradeoff
datatable_archiving_maximum_rows_actions500250 or 100More long-tail pages land under "Others".
datatable_archiving_maximum_rows_subtable_actions10050More child rows under each page land under "Others".
datatable_archiving_maximum_rows_events500250 or 100Event categories/actions/names with a long tail are compressed.
datatable_archiving_maximum_rows_site_search500250 or 100Long-tail internal search terms are compressed.
ini
[General]
datatable_archiving_maximum_rows_actions = 250
datatable_archiving_maximum_rows_subtable_actions = 50
datatable_archiving_maximum_rows_events = 250
datatable_archiving_maximum_rows_site_search = 250

Everything past the cap is rolled into the "Others" row, which is almost always what you want for a directory of one-hit URLs nobody analyses individually. The events and site-search caps matter only when those reports also have high cardinality. These limits affect newly processed archives; if you expect historical report shape to change, invalidate and reprocess the affected site/date range deliberately rather than the whole install.

4. Cut URL and action cardinality at the source

Capping the table keeps memory bounded. Cutting cardinality makes every run faster and the archives smaller. The biggest single win is excluding noisy query parameters. As a Super User, go to Administration → Websites → Manage and set per-site "Excluded Parameters", or set the "Global list of Query URL parameters to exclude" in the global website settings. Strip session IDs, click IDs, and anything that makes otherwise-identical pages look unique:

text
sessionid
PHPSESSID
fbclid
gclid
utm_term

That turns a stream like /download?id=1, /download?id=2, and /download?id=10000 back into one report row when id is not analytically useful. If the parameter changes the content, do not exclude it; normalize only the noise that creates fake uniqueness.

Note that exclusions only affect data going forward, so they shrink future archives, not the rows already stored. For log imports specifically, collapse directory-style and one-hit URLs before importing rather than letting thousands of unique paths land in the database.

5. Keep segments under control

Matomo archives by site, period, and segment. Each pre-processed segment adds another set of segment archives for the sites and periods it applies to. A handful of segments you genuinely watch is fine. Ten or forty stale segments that someone created during an experiment two years ago can turn one archive queue into many queues. Audit them and delete the ones nobody reads.

6. Clear stuck archivers and the invalidation backlog

This is the fix for the "CPU spiked for no reason" morning. A sustained jump in MySQL CPU and PHP-FPM memory with flat traffic almost always means archiving is looping, not tracking misbehaving. The usual cause is a small set of invalidations (frequently year-period reports, or one plugin's reports) being reprocessed on every run, so each cron cycle stacks more climulti:request processes that never finish.

bash
# See exactly which idSite / period / segment is slow
./console core:archive --url=https://your-matomo.example -vvv
 
# Re-process a scoped set instead of invalidating everything at once
./console core:invalidate-report-data --help

Inspect <tables_prefix>archive_invalidations, not necessarily a literal table named archive_invalidations. The prefix is in the [database] section of config/config.ini.php; many installs use matomo_.

Make sure two cron runs can't overlap by using a lock file or one scheduler entry. Only kill a process after you have confirmed it is stale: old PID age, matching command, no fresh log output, and no movement in the verbose archive. Then let one clean run catch up. Use core:invalidate-report-data to reprocess a specific site and date range rather than invalidating the whole install and setting off a stampede. Run it with --help first, since the exact flags move between point releases.

7. Scale the database and parallelism for many sites

At large site counts, the database often becomes the limiting factor. Check DB CPU, I/O wait, slow queries, and the invalidation backlog before increasing archiver concurrency. Add concurrency only when the database has headroom; reduce it or tune the database first when MySQL/MariaDB is already saturated.

Increase concurrency when...Tune or reduce concurrency when...
PHP archivers are idle or waiting and DB CPU/I/O still has room.DB CPU is pinned, I/O wait is high, or slow queries pile up.
The invalidation backlog is large but each archive finishes cleanly.The same site/period/segment repeats on every run.
You have enough workers and memory for parallel CLI processes.Parallel runs push PHP into swap or make reports slower.
bash
# Run several archivers in parallel, with bounded
# concurrency per website. concurrent-requests-per-website defaults to 3;
# concurrent-archivers is not 3 by default, so set it deliberately.
./console core:archive --url=https://your-matomo.example \
  --concurrent-archivers=3 --concurrent-requests-per-website=3
ini
# In your MySQL/MariaDB config, on a dedicated DB host:
# size the buffer pool to roughly 60-80% of that server's RAM
innodb_buffer_pool_size = 24G

Put MySQL/MariaDB on its own server for larger installs, size innodb_buffer_pool_size to roughly 60–80% of RAM on a dedicated DB host, and parallelise archiving with --concurrent-archivers only after you have measured DB headroom. Keeping the invalidation backlog small (step 6) makes scaling much less fragile. If you're getting raw DB errors rather than just slowness, our notes on fixing Matomo database errors cover the table-level problems that show up under this kind of load, and the guide on running multiple sites and measurables covers the multi-site setup itself.

Confirm it worked

  • Run ./console core:archive --url=… -vvv and watch the log name the exact idSite / period / segment that's slow.
  • Confirm month and year periods now complete without a DataTable.php fatal.
  • After disabling browser archiving, check the UI still shows fresh data. It now reads pre-archived reports, and time_before_today_archive_considered_outdated (default 900 seconds) controls how stale "today" is allowed to be.
  • Watch MySQL CPU and PHP memory drop back to baseline between cron runs instead of staying pinned.

Prevent regressions

Run a modern PHP 8.x line. Matomo 5 works well with PHP 8, and the latest 8.x release is the most memory-efficient and fastest, which directly helps archiving headroom. Enable OPcache. Stay on the latest Matomo 5.x, since several archiving and invalidation-query performance fixes have landed across point releases. And periodically re-audit your segment and custom-report counts, because both creep upward and both tax every run.

What we'd actually do

Move archiving to cron and turn off browser triggering first. That one change removes most of the surprise load and makes everything else measurable. Then, the moment you see a DataTable.php fatal, skip straight to capping the row limits and excluding query parameters. Don't spend a week walking memory_limit up by gigabytes; the limit was never the thing. For a large multi-site install, give the database its own tuned server, run archivers in parallel, and keep the invalidation backlog short so it stays that way.

Martez connects Matomo with Meta Ads and Google Ads so ROAS, CLV, and attribution sit next to your web analytics instead of in a separate spreadsheet. It's in private beta. Join the waitlist if that's relevant to you.

More memory was never the fix. Smaller tables were.