Transmission modes
How hits reach Matomo is one setting, and nothing at a call site changes when you change it:
MATOMO_MODE=batch
| Mode | Behavior |
|---|---|
queue (default) | A request's hits are sent as one queued Bulk request on terminate. |
sync | Sent immediately — handy for the CLI, tests, or very low volume. |
batch | Hits are buffered across requests and flushed in large Bulk batches — the most resource-efficient option. |
queue — the default
Hits accumulate in a per-request buffer as you call the facade. When the framework terminates the request, they are dispatched as one queued job carrying one Bulk request. Ten tracked events in a request means one HTTP call to Matomo, made by a worker after the response has been sent.
'queue' => [
'connection' => env('MATOMO_QUEUE_CONNECTION'), // null = default connection
'queue' => env('MATOMO_QUEUE', 'matomo'),
'tries' => 5,
'backoff' => [30, 120, 300, 900],
'retry_until_minutes' => 1440,
],
Your worker must serve the matomo queue. This is the one operational
requirement, and it is the most common reason a correctly configured package
produces no data:
php artisan queue:work --queue=matomo,default
The dedicated queue name is deliberate — analytics should never sit behind a slow
mail job, and a Matomo outage should never starve your application's real work.
Point MATOMO_QUEUE at default if you would rather not run a second queue.
Retries escalate: 30 seconds, 2 minutes, 5 minutes, 15 minutes, and then hold at that
last step, for up to queue.tries attempts. A batch that spends its budget is moved to
the dead-letter store rather than being retried further. queue.retry_until_minutes is
the outer bound and, on the defaults, is never reached. See
reliability for what happens when they run out.
sync — send immediately
Every hit is one HTTP request, made inline. That blocks the caller for the duration of the request to Matomo, which is why it is not the default.
It is the right mode for a CLI command where there is no response to protect, for a low-volume application where a queue worker is more infrastructure than the traffic justifies, and inside tests where you want the send to have happened by the time the assertion runs.
batch — buffer across requests
Hits go into a durable cross-request buffer instead of being dispatched per request. A separate process drains the buffer and sends it in large Bulk batches. At volume this is by far the most efficient option: hundreds of hits from dozens of requests become one HTTP call.
'batch' => [
'driver' => env('MATOMO_BATCH_DRIVER', 'database'), // database|redis|file|array
'size' => env('MATOMO_BATCH_SIZE', 200),
'flush_interval' => env('MATOMO_BATCH_INTERVAL', 60),
'max_per_flush' => 2000,
'stale_after_minutes' => 15,
'max_attempts' => env('MATOMO_BATCH_MAX_ATTEMPTS', 25),
],
The drivers
| Driver | Store | Use it when |
|---|---|---|
database | A table in your database (the default) | You want durability with no extra infrastructure. |
redis | A Redis list, drained as a reliable queue | You have Redis, want the buffer off your primary database, and the instance is set to noeviction or a volatile-* policy — see below. |
file | A JSONL spool on disk, claimed by atomic rename | A single node, or when neither of the above is available. |
array | Memory, for the current request only | Tests. Never in production — the process exit loses the hits. |
database needs the migration. The package registers its migrations
automatically, so php artisan migrate is enough — see
installation.
All three durable drivers claim a batch before sending and only remove it once
Matomo confirms a 200, so a crash mid-send re-delivers rather than loses. A claim
that is never acknowledged is reclaimed after stale_after_minutes.
The redis driver needs a non-evicting instance
That durability holds for redis only while Redis is not allowed to evict the buffer's
keys. They carry no TTL — they are pending work, not cache — so under allkeys-lru,
allkeys-lfu or allkeys-random they are as evictable as anything else in the keyspace,
and an eviction looks like nothing: the claim comes back empty, matomo:flush prints
Flushed 0 Matomo hit(s). and exits zero, which is what an idle minute prints too.
matomo:test reads the policy and warns when it is one of the three. The full explanation
is in reliability.
Draining the buffer
In batch mode the package registers a scheduled matomo:flush for you — it runs
every minute and is guarded so two runs cannot overlap. Make sure your scheduler
runs; without it, batch mode fills the buffer and sends nothing.
For a high-volume file or Redis spool, a long-running drainer beats a per-minute schedule:
php artisan matomo:work --max-time=3600 --memory=256
matomo:work loops with flush_interval seconds between passes. --max-time and
--memory let a supervisor recycle it before it grows unbounded, the same pattern
queue:work uses. Run one instance — the atomic claims make concurrent runs
safe from double-sending, but they do not make them useful.
--once and --max-runs are there for a cron-driven or test-driven drain.
Sizing
size is how many hits go into one Bulk request; max_per_flush caps how many a
single flush pass will move in total, so one pass over a large backlog cannot run
unbounded. Raising size reduces HTTP calls but makes each one bigger — and a Bulk
request that exceeds what your Matomo accepts fails as a unit. The default of 50 is
conservative; measure with the load simulator before raising it.
Choosing
Start on queue. It is correct for the vast majority of applications, needs only a
worker you probably already run, and never blocks a response.
Move to batch when the number of Bulk requests per minute becomes the thing you
want to reduce — typically once you are tracking most page views server-side on a
busy site. It costs you a buffer to operate and a scheduler to watch, and it widens
the window between an event happening and Matomo knowing about it.
Use sync for the CLI and for tests.
In
queueandbatchmode, setMATOMO_TOKEN. Without a token Matomo timestamps each hit when it arrives, and in these modes that can be minutes — or, after a retry or a dead-letter replay, hours — after the event actually happened. See installation.