# 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.

Published: 2026-07-15
Categories: Matomo Self-Hosting
Canonical: https://martez.io/blog/matomo-archiving-memory-cpu-scaling

---

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.

<Callout type="tip" title="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.

</Callout>

## 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](/blog/matomo-import-logs-not-working) is usually where the unique-URL explosion got into the database in the first place. Worth tracing before you tune anything.

<FlowSteps
  caption="Where archiving memory is spent"
  steps={[
    { title: "Tracking requests" },
    { title: "Raw log tables" },
    {
      title: "core:archive cron",
      branch: {
        label: "if browser archiving is enabled",
        title: "Browser report requests trigger archiving here too",
        tone: "muted",
      },
    },
    { title: "In-memory DataTables", detail: "this is where the memory goes" },
    { title: "Archive tables" },
    { title: "UI and API reports" },
  ]}
/>

## 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](/blog/set-up-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
```

<Callout type="question" title="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.</Callout>

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.

| Setting | Matomo 5 default | Pathological-site trial | Report tradeoff |
| --- | ---: | ---: | --- |
| `datatable_archiving_maximum_rows_actions` | `500` | `250` or `100` | More long-tail pages land under "Others". |
| `datatable_archiving_maximum_rows_subtable_actions` | `100` | `50` | More child rows under each page land under "Others". |
| `datatable_archiving_maximum_rows_events` | `500` | `250` or `100` | Event categories/actions/names with a long tail are compressed. |
| `datatable_archiving_maximum_rows_site_search` | `500` | `250` or `100` | Long-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_`.

<Callout type="info" title="Read-only invalidation checks" collapsable>

```sql
SELECT COUNT(*) AS pending_invalidations
FROM <tables_prefix>archive_invalidations;

SELECT *
FROM <tables_prefix>archive_invalidations
WHERE ts_started IS NOT NULL
ORDER BY ts_started ASC
LIMIT 50;
```

Rows with `ts_started IS NOT NULL` are not automatically bad. Compare the timestamp with live `core:archive` / `climulti:request` PIDs, Matomo logs, and whether the verbose archive output is still progressing before you kill anything.

</Callout>

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](/blog/fix-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](/blog/matomo-multiple-sites-domains-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](/?utm_source=martez&utm_medium=blog&utm_campaign=matomo-archiving-memory-cpu-scaling) 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](/signup?utm_source=martez&utm_medium=blog&utm_campaign=matomo-archiving-memory-cpu-scaling) if that's relevant to you.

More memory was never the fix. Smaller tables were.
