.. include:: /Includes.rst.txt
.. _performance-limitations:
===========
Limitations
===========
This chapter lists what the approach cannot do and which failure modes to plan for.
:ref:`performance-model` has the matrix of what each configuration does; this chapter
assumes it.
.. _performance-limitations-relative-lifetime:
Only a relative lifetime is available
=====================================
TYPO3's cache API accepts "keep this for N seconds", not "keep this until timestamp T".
``ModifyCacheLifetimeForPageEvent`` is the only lever the extension has over a page cache
entry, and it takes a duration.
The event does supply the page id (``getPageId()``) and the rendering instructions, so the
extension can and does scope by page — but it says nothing about which records were
rendered into the entry.
The extension therefore has to infer the relevant transitions from the configured scope
rather than from the page's actual content.
.. _performance-limitations-synchronized-expiry:
Synchronized expiry with global scoping
=======================================
With ``scoping.strategy = global`` every page cache entry written in the same second
receives the same expiry timestamp: the earliest upcoming transition anywhere on the site.
.. code-block:: text
Site with 10,000 pages
Page A has a future starttime at 10:00
Every other page has no temporal restriction
Every page cache entry written before 10:00 expires at 10:00.
After 10:00 they all miss, and each miss regenerates a page.
Two consequences follow.
Cache hit ratio
The effective lifetime of the whole page cache becomes the gap between transitions.
On a site with frequent transitions that is far shorter than the lifetime the site would
otherwise use.
Simultaneous misses
Because the expiry is identical rather than staggered, misses arrive together.
A CDN or reverse proxy in front of TYPO3 respects the same ``Cache-Control`` window, so
the burst reaches the origin as one wave.
Mitigations
-----------
- Switch to ``per-page`` scoping.
Content transitions then expire only the page carrying the content; page transitions
still land on every entry, since they change menus everywhere.
- Switch to ``scheduler`` timing.
Nothing expires by time at all; the task flushes what the scoping strategy names.
- Serve stale content while regenerating, so the burst does not reach the origin at full
size.
.. code-block:: apache
:caption: Apache
Header set Cache-Control "public, max-age=3600, stale-while-revalidate=300"
.. code-block:: text
:caption: Varnish VCL
sub vcl_backend_response {
set beresp.grace = 5m;
}
.. code-block:: nginx
:caption: Nginx rate limiting on the origin
limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;
limit_req zone=one burst=20 nodelay;
- Warm the cache before the transition, using the transition times
``temporalcache:list`` reports.
.. _performance-limitations-query-cost:
Query cost on every cache write
===============================
With ``dynamic`` timing, each page cache write runs two ``MIN()`` queries per monitored
table — four with the default ``pages`` and ``tt_content``.
.. code-block:: sql
:caption: The shape of each query (per-page scoping adds the pid clause)
SELECT MIN(`starttime`) AS min_transition
FROM `tt_content`
WHERE `starttime` > :now
AND `deleted` = 0
AND `hidden` = 0
AND `pid` = :pageId
AND (`t3ver_wsid` = 0 OR `t3ver_wsid` IS NULL)
AND `sys_language_uid` = :language
Notes on that query:
- Records with ``starttime = 0`` are excluded by the ``> :now`` comparison; there is no
separate ``!= 0`` clause.
- The ``deleted``/``hidden`` column names come from the table's TCA ``ctrl`` section.
Where no TCA is loaded, those clauses are simply absent.
- The workspace clause is ``t3ver_wsid = :workspace`` for any workspace other than live.
Indexes
-------
:file:`ext_tables.sql` ships the matching composite indexes for the default tables, so no
manual ``CREATE INDEX`` is needed — run the database analyzer after installing and confirm
with ``temporalcache:verify``:
- ``pages``: ``idx_temporalcache_starttime (starttime, sys_language_uid)``,
``idx_temporalcache_endtime (endtime, sys_language_uid)``
- ``tt_content``: the same two
.. warning::
A table registered through ``TemporalMonitorRegistry`` gets **no** index from this
extension, and adds two queries to every lookup.
Ship the equivalent index with the extension that registers the table.
Only the site-wide lookup is memoized, in a request-scoped singleton keyed by timestamp,
workspace and language.
The per-page and per-content lookups are not memoized.
.. _performance-limitations-scheduler-cost:
The scheduler task is not free
==============================
``scheduler`` timing removes the per-request queries, but its task loads **every** record
that carries a ``starttime`` or ``endtime`` from every monitored table into PHP on each run
— one query per table, with no time restriction in SQL — and then filters the run's window
in PHP.
Its cost scales with the total volume of temporal content on the site, not with the number
of transitions that actually occurred.
On a site with a lot of scheduled content, running the task every minute repeats that load
every minute.
The first run has no stored timestamp and therefore processes the range from epoch to now,
which flushes for every past transition once.
.. _performance-limitations-scheduler-scope:
Scheduler flushes are narrower than they look
=============================================
Under ``scheduler`` or ``hybrid`` timing the flush tags come from the scoping strategy:
- ``global`` flushes the ``pages`` tag — everything.
- ``per-page`` flushes ``pageId_`` for a page and ``pageId_`` for a content
element.
- ``per-content`` flushes one ``pageId_*`` per refindex hit; a page record still yields only
its own tag.
So with ``per-page`` or ``per-content`` scoping, a page reaching its ``starttime`` refreshes
that page's own cache and nothing else.
Menus on other pages keep showing the old page tree until their entries expire for another
reason.
Only ``global`` scoping refreshes them.
.. _performance-limitations-per-content-lifetime:
Per-content scoping does not narrow lifetimes
=============================================
``PerContentScopingStrategy::getNextTransition()`` returns the site-wide transition, the
same value ``global`` returns.
Narrowing it per page would risk serving stale content that was embedded from elsewhere.
Consequence: ``per-content`` combined with ``dynamic`` timing is indistinguishable from
``global`` in its cache effect.
The strategy's precision is in its flush tags and needs ``scheduler`` or ``hybrid`` timing
to have any effect.
.. _performance-limitations-cross-page:
Cross-page dependencies with dynamic timing
===========================================
``per-page`` scoping looks at content elements by ``pid``.
An element rendered onto another page through a ``CONTENT`` or ``RECORDS`` cObject is
therefore invisible to that page's lifetime calculation, and the embedding page keeps its
long lifetime when the element transitions.
There is no configuration that fixes this for ``dynamic`` timing.
``per-content`` scoping resolves the references, but only for flush tags.
.. _performance-limitations-hybrid:
A hybrid combination that drops transitions
===========================================
``timing.hybrid.pages`` and ``timing.hybrid.content`` both accept ``dynamic`` and
``scheduler``, so four combinations are configurable.
One of them does not work:
.. list-table::
:header-rows: 1
:widths: 20 20 60
* - ``pages``
- ``content``
- Effect
* - ``dynamic``
- ``scheduler``
- The documented pairing. Lifetimes are shortened; content transitions are flushed by
the task.
* - ``dynamic``
- ``dynamic``
- Equivalent to ``timing.strategy = dynamic``.
* - ``scheduler``
- ``scheduler``
- Equivalent to ``timing.strategy = scheduler``.
* - ``scheduler``
- ``dynamic``
- **Content transitions are dropped.** The lifetime is ``null`` because the ``pages``
rule decides it, and the task hands content transitions to the dynamic strategy,
whose ``processTransition()`` does nothing.
.. _performance-limitations-scheduler-context:
The scheduler task sees only live and the default language
==========================================================
``TemporalCacheSchedulerTask`` calls ``findTransitionsInRange()`` without a workspace or
language argument, so it runs with the defaults: workspace ``0`` and
``sys_language_uid = 0``.
Transitions on translated records are therefore not processed by ``scheduler`` or
``hybrid`` timing.
On a multi-language site, use ``dynamic`` timing — which reads workspace and language from
the request context — for the languages that matter.
.. _performance-limitations-scope-of-effect:
Only the page cache
===================
The listener sets the page cache lifetime.
Caches written by other code — an extension's own cache, a reverse proxy configured
independently, a static file cache — keep whatever lifetime that code assigns.
Granularity is one second, because transitions are Unix timestamps.
.. _performance-limitations-no-filtering:
No per-page-tree switch
=======================
The configuration is global to the installation.
There is no setting to exclude a page tree, a doktype or a content type from the
calculation.
The workaround is a second listener on the same event, ordered after this extension's, that
overrides the lifetime for the pages it wants to exclude — see
:ref:`architecture-custom-listener`.
.. _performance-limitations-next-steps:
Next steps
==========
- :ref:`performance-strategies` — the settings that change this behavior
- :ref:`decision-guide` — which configuration matches which site
- :ref:`architecture` — the implementation these limits come from
- :ref:`phases` — what a core solution would change