Cache and performance
CPalius's performance strategy is layered; each layer can be switched on and off independently and none is mandatory for the core to work (the site opens without Redis and without Varnish).
browser ─▶ [CDN] ─▶ [Varnish] ─▶ web server ─▶ [origin HTML cache] ─▶ PHP/Symfony ─▶ [Redis/application cache] ─▶ database
Anonymous traffic is sessionless
The precondition of caching: if the visitor is anonymous, no session cookie is set. A response with a cookie cannot be put into a cache shared by everyone. For this reason counters, votes and notification states must not be tied to the session (see the Checklist).
Origin HTML cache
This is the core's own page cache; it works without installing Varnish or a CDN. It caches only anonymous GET requests; never admin, authentication and form endpoints.
| Setting | Default | |
|---|---|---|
performance.cpalius.ttl |
86400 | Snapshot lifetime (s) |
performance.cpalius.excludes |
/aacp /admin /login /logout /hesap /api /_fragment /_profiler /_wdt |
Path prefixes that will not be cached |
performance.cpalius.minify |
on | HTML/CSS/JS minification |
performance.cpalius.compress_assets |
on | Static asset compression |
performance.cpalius.compress_images |
on | Compressing local images |
performance.cpalius.shield |
off | Source code shield (compresses HTML and makes it unreadable) |
Snapshots are written after the response has been sent (kernel.terminate); the visitor does not wait for the cost of writing. Expired ones are cleaned by the cpalius.origin_cache.purge task (every 15 min).
Tagged invalidation (cache tags)
Deleting the whole cache on every change is both slow and unnecessary. The core uses the cache tag idea known from Drupal, but without a rebuild step: tags are plain strings that are not registered in advance.
A controller says what its response depends on:
$this->tags->addEntityTag('node', $node->getId()); // "node:42"
$this->tags->addListTag('node', 'post'); // "node_list:post"
$this->tags->addConfigTag('blog.settings'); // "config:blog.settings"
$this->tags->setMaxAge(3600); // optional upper limit
At the end of the request OriginCacheWriter reads these tags and indexes the snapshot with them. When a Node, User or Term is saved/deleted, EntityCacheTagInvalidator automatically deletes only the snapshots that carry that entity and the list tag. So if your own entity implements FieldableInterface, you do not need to call purgeAreas() by hand in the controller.
When manual clearing is needed:
$purger->purgeAll(); // everything (e.g. after an import)
$purger->purgeTags('node:42'); // by tag
$purger->purgeAreas('blog'); // by area name
Cache invalidation is a hook listener; if it fails it is isolated and does not block saving the entity.
Varnish
If Varnish is in front of you, CPalius gives it the correct Cache-Control headers. Admin and authentication paths are always private; the operator's performance.varnish.excludes list is an additional prefix.
| Setting | Default |
|---|---|
performance.varnish.backend_url |
http://127.0.0.1/ |
performance.varnish.port |
6081 |
performance.varnish.ttl |
120 |
performance.varnish.timeout |
2 |
Redis, Memcached, Nginx PageSpeed: backend management
The AACP → Maintenance → Performance screen (PerformanceBackendRegistry) manages each backend with a live connection test. An important rule: a backend can only be enabled if the last recorded test succeeded. This prevents a wrong Redis setting from bringing the site down.
| Backend | Settings |
|---|---|
| Redis | host, port (6379), password, timeout |
| Memcached | host, port (11211), timeout |
| Nginx PageSpeed | status check address |
| Varnish | above |
The dashboard carries system widgets that show the status of these services.
If there is no Redis
If the Redis client (phpredis) is not installed or the connection cannot be opened, the service returns null and consumers treat it as "Redis offline". The reason: an ordinary dependency edge would bring every route (including the recovery console) down to 500 when the extension is missing. That is why OptionalRedis exists. If Redis is used for sessions, a failover session handler takes over.
Application cache and rebuilding
Frequently read data such as settings, field definitions and the role map are kept in cache.app and invalidated with clearCache() / invalidate() on write.
CacheRebuildManager does separate jobs from the panel; they are independent:
- Clear the Symfony cache (
clearSymfonyCache) - Reset OPcache (
resetOpcache) - Rebuild assets (
rebuildAssets,compileMappedAssets) - After a code update all in sequence (
afterCodeUpdate)
If clearing the cache fails, a marker file (cache-purge-failed.json) is left and the panel shows it; there is no silent failure.
Counter rebuild (Rebuilder)
A module does expensive counts like COUNT(*) not on the write path but as a separate job. A class implementing RebuilderInterface defines a named "recount" job (id, name, total, batch size, priority, rebuild($offset, $limit)). The AJAX runner in Studio calls the job in batches: each batch returns quickly and is called again with the next offset. Modules register, and the core does not import them.
N+1 guard
In the development (and test) environment, if more than 10 queries go to the same table in a single request, MaxQueriesExceededException is thrown (a limit per table). This forces you to write a fetch join or bulk loading. Helpers:
cp_fields_preload(items)(Twig) for field referencesQueryScopeApplier(turned into SQL) for the permission filternode_field_indexfor the field index
The N+1 guard is active only in the dev (and test) environment; it is off in production. There is a reason for this: if it were active in production it would silently break origin cache writing. Catch the problem while developing.
A practical recipe
- Small site: install nothing. The origin HTML cache +
cache.appis enough. - Medium site: add Redis (session and application cache).
- Large/high-traffic site: put Varnish or a CDN in front, copy media to remote storage (Media and storage).
- After every change, run the test in AACP → Performance.