Summary
The object cache silently writes at most 50 items per request, hardcoded and not configurable through any config/DI/env option. Make the limit configurable and log when items are dropped.
Problem
protected int $maxWriteToCacheItems = 50; (lib/Cache/Core/CoreCacheHandler.php:122). The only mutator is setMaxWriteToCacheItems() (:230), which has no caller and no config binding anywhere in lib/ or bundles/.
Flow for a normal element load:
Cache::save() queues the item (non-forced path) into $saveQueue (:325).
- Mid-request, when the queue exceeds
3 × 50 = 150, cleanupQueue() trims it back to 50 (:340, :361) — memory protection.
- On shutdown,
writeSaveQueue() calls cleanupQueue() unconditionally before the write loop (:813), so at most 50 items ever reach the backend, regardless of queue size.
public function cleanupQueue(): void
{
uasort($this->saveQueue, fn (CacheQueueItem $a, CacheQueueItem $b) => $b->getPriority() <=> $a->getPriority());
array_splice($this->saveQueue, $this->maxWriteToCacheItems); // silent drop, no log
}
Elements save with default priority 0, so beyond 50 the survivors are arbitrary. Consequence: any request touching >50 elements persists at most 50 — the rest reload cold on every subsequent request, regardless of cache pool capacity. The drop is invisible (no log, no metric).
Measured (demo, DB cache backend; warm one process, measure a fresh cold load; 286 objects → ~800 write candidates)
| Scenario |
cache_items rows |
Next load: queries |
Next load: time |
| Stock (limit 50) |
102 |
5,069 |
4,841 ms |
| Limit raised |
1,074 |
576 |
1,055 ms |
| Cache disabled (baseline) |
— |
4,436 |
~4,360 ms |
Note the middle row vs the baseline: with the cap at 50, the "cached" run is slower than no cache at all (5,069 > 4,436 queries) — every miss pays a cache-lookup round-trip on top of full rehydration. On the default backend the hardcoded cap makes the object cache a net negative for element-heavy requests.
Proposed solution
-
Config binding (default stays 50, so BC):
pimcore:
cache:
max_write_items: 50 # -1 = unlimited
handle_cli: false # allow cache writes from CLI (also currently a setter-only flag)
wired via calls: [[setMaxWriteToCacheItems, ['%pimcore.cache.max_write_items%']], [setHandleCli, ['%pimcore.cache.handle_cli%']]] in cache.yaml.
-
Log the drop in cleanupQueue() (count + first N keys, rate-limited per process) so poor hit rates become diagnosable.
-
(Optional follow-up) deterministic element priority tiers so that when a limit is hit, eviction is predictable rather than arbitrary.
The 50-item bound was reasonable for the old DB cache backend; on Redis it is far too low. Keeping it configurable (not removing it) preserves the shutdown-latency/memory protection for those who need it.
Effort
S — config node, DI wiring, one log line, tests. No core-logic change (the setter already exists — the benchmark above raised the limit at runtime without patching core).
Related
Part of the performance audit; companion ticket: permission-check memoization (separate issue).
Summary
The object cache silently writes at most 50 items per request, hardcoded and not configurable through any config/DI/env option. Make the limit configurable and log when items are dropped.
Problem
protected int $maxWriteToCacheItems = 50;(lib/Cache/Core/CoreCacheHandler.php:122). The only mutator issetMaxWriteToCacheItems()(:230), which has no caller and no config binding anywhere inlib/orbundles/.Flow for a normal element load:
Cache::save()queues the item (non-forced path) into$saveQueue(:325).3 × 50 = 150,cleanupQueue()trims it back to 50 (:340,:361) — memory protection.writeSaveQueue()callscleanupQueue()unconditionally before the write loop (:813), so at most 50 items ever reach the backend, regardless of queue size.Elements save with default priority 0, so beyond 50 the survivors are arbitrary. Consequence: any request touching >50 elements persists at most 50 — the rest reload cold on every subsequent request, regardless of cache pool capacity. The drop is invisible (no log, no metric).
Measured (demo, DB cache backend; warm one process, measure a fresh cold load; 286 objects → ~800 write candidates)
cache_itemsrowsNote the middle row vs the baseline: with the cap at 50, the "cached" run is slower than no cache at all (5,069 > 4,436 queries) — every miss pays a cache-lookup round-trip on top of full rehydration. On the default backend the hardcoded cap makes the object cache a net negative for element-heavy requests.
Proposed solution
Config binding (default stays 50, so BC):
wired via
calls: [[setMaxWriteToCacheItems, ['%pimcore.cache.max_write_items%']], [setHandleCli, ['%pimcore.cache.handle_cli%']]]incache.yaml.Log the drop in
cleanupQueue()(count + first N keys, rate-limited per process) so poor hit rates become diagnosable.(Optional follow-up) deterministic element priority tiers so that when a limit is hit, eviction is predictable rather than arbitrary.
The 50-item bound was reasonable for the old DB cache backend; on Redis it is far too low. Keeping it configurable (not removing it) preserves the shutdown-latency/memory protection for those who need it.
Effort
S — config node, DI wiring, one log line, tests. No core-logic change (the setter already exists — the benchmark above raised the limit at runtime without patching core).
Related
Part of the performance audit; companion ticket: permission-check memoization (separate issue).