---
title: "Optimization strategies"
manual: "Temporal Cache Management"
version: "main"
source: "Performance/Strategies.rst"
rendered: "2026-10-07T14:40:31+00:00"
---

# Optimization strategies {#performance-strategies}

Three settings change how much cache the extension costs:

1.  **Scoping** — which records a lookup covers and which tags a transition flushes
1.  **Timing** — whether invalidation happens through a shortened lifetime or a background
    task
1.  **Harmonization** — a one-off rewrite of the stored `starttime`/`endtime` values so
    fewer distinct transition moments exist

Scoping and timing interact, and one combination is a trap; see [The model in one table](Index.html#performance-model)
for the full matrix before reading on.

## Scoping strategies {#performance-strategies-scoping}

### Global (default) {#performance-strategies-scoping-global}

**config/system/additional.php**

```php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['scoping']['strategy'] = 'global';
```

-   **Lifetime**

    `getNextTransition()` returns the earliest upcoming transition across every monitored
    table, site-wide.
    The page id is ignored, so every page cache entry written in that second gets the same
    expiry.

-   **Flush tags**

    `['pages']` (`ScopingStrategyInterface::ALL_PAGES`). TYPO3 does not put this tag on
    page cache entries, so the scheduler timing strategy empties the whole page cache for
    it instead of flushing by tag.

-   **Trade-off**

    Nothing to configure and nothing can be missed.
    Every transition anywhere costs the whole page cache.

### Per-page {#performance-strategies-scoping-per-page}

**config/system/additional.php**

```php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['scoping']['strategy'] = 'per-page';
```

-   **Lifetime**

    The earlier of two lookups: the next transition in the `pages` table site-wide, and the
    next transition in the content tables restricted to `pid = <rendered page>`.
    Page transitions stay site-wide on purpose — a page appearing or disappearing changes
    menus on every page.
    When no page id is available the strategy falls back to the site-wide lookup.

-   **Flush tags**

    `pageId_<uid>` for a page record, `pageId_<pid>` for a content element.

-   **Trade-off**

    Content churn is confined to the page that carries the content.
    Content embedded from another page through `CONTENT` or `RECORDS` cObjects is not
    seen, so that page's lifetime is not shortened when the embedded element transitions.

### Per-content {#performance-strategies-scoping-per-content}

**config/system/additional.php**

```php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['scoping'] = [
    'strategy' => 'per-content',
    'use_refindex' => true,
];
```

-   **Lifetime**

    Identical to `global`: the site-wide next transition.
    `PerContentScopingStrategy::getNextTransition()` deliberately does not narrow, because
    an element can be embedded into arbitrary pages and a narrowed lifetime could serve
    stale embedded content.

-   **Flush tags**

    One `pageId_*` tag per page that `sys_refindex` reports as referencing the element,
    which covers direct placement, `CONTENT`/`RECORDS` embedding, mount points and
    shortcuts.
    A page record always yields its own tag only, and a record of another registered
    table the tag of its own `pid` (the refindex lookup resolves `tt_content` only).

-   **Trade-off**

    The most precise invalidation available, but only along the flush-tag path.

> [!WARNING]
> `per-content` with the default `dynamic` timing changes nothing at all — that
> combination reads only the lifetime, which `per-content` leaves site-wide.
> Pair it with `scheduler` or `hybrid` timing, or the refindex work never runs.

Set `scoping.use_refindex = 0` to skip the refindex lookup; the strategy then falls back
to the element's own `pid`, which is what `per-page` already does. The strategy also
falls back to the `pid` when the refindex returns nothing or the lookup throws, so a stale
`sys_refindex` degrades quietly rather than failing.

**Keep the reference index current**

```bash
vendor/bin/typo3 referenceindex:update
```

## Timing strategies {#performance-strategies-timing}

### Dynamic (default) {#performance-strategies-timing-dynamic}

**config/system/additional.php**

```php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['timing']['strategy'] = 'dynamic';
```

The listener asks the scoping strategy for the next transition on every page cache write
and sets the lifetime to the remaining seconds, capped at
`advanced.default_max_lifetime`.
With no upcoming transition the lifetime is `advanced.default_max_lifetime`; with a
transition already in the past it is `60`.

Cost: two `MIN()` queries per monitored table on every cache write — four with the default
`pages` and `tt_content`.
Only the site-wide lookup is memoized for the duration of a request, and its cache key
includes the current timestamp, so the memo helps within one second.

Transitions take effect at the moment they happen, on the next request to the page.
The scheduler is not involved and no task has to be set up.

### Scheduler {#performance-strategies-timing-scheduler}

**config/system/additional.php**

```php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['timing']['strategy'] = 'scheduler';
```

`getCacheLifetime()` returns `null`, so the listener leaves TYPO3's own lifetime
untouched and page generation carries no extra query.
Invalidation moves to `Netresearch\TemporalCache\Task\TemporalCacheSchedulerTask`:

1.  Add the task in the Scheduler backend module and note the UID it receives.
1.  Let cron run the Scheduler.

**Crontab**

```text
* * * * * php /path/to/typo3/vendor/bin/typo3 scheduler:run
```

> [!NOTE]
> `scheduler:run` executes every task that is due.
> To run only this one, pass its UID: `scheduler:run --task=<uid>`.
> There is no dedicated console command for the transition check — the extension's own
> commands are `temporalcache:analyze`, `temporalcache:verify`,
> `temporalcache:harmonize` and `temporalcache:list`.

Each run reads its last-run timestamp from TYPO3's `Registry` (namespace
`tx_temporalcache`, key `scheduler_last_run`), processes every transition since then,
and stores the new timestamp.
How often that happens is the task's frequency in the Scheduler module.

> [!WARNING]
> A transition is only noticed on the next task run, so content appears or disappears up
> to one interval late.
> The very first run has no stored timestamp and therefore treats the range as starting at
> epoch, which means every past transition on the site is processed once.

### Hybrid {#performance-strategies-timing-hybrid}

**config/system/additional.php**

```php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['timing'] = [
    'strategy' => 'hybrid',
    'hybrid' => [
        'pages' => 'dynamic',
        'content' => 'scheduler',
    ],
];
```

Two switches, each accepting `dynamic` or `scheduler`.
There is no per-table setting: a record is classified as `page` (table `pages`) or
`content` (every other monitored table).

`timing.hybrid.pages` decides two things — how page transitions are processed by the
scheduler task, **and** which strategy computes the cache lifetime.
`timing.hybrid.content` decides only how content transitions are processed by the task.

> [!NOTE]
> Because the lifetime always follows the `pages` rule, `pages = dynamic` keeps the
> per-request `MIN()` queries, and those queries cover the content tables too.
> Hybrid with `pages = dynamic` does not remove query cost for content; it changes who
> reacts to content transitions.

> [!WARNING]
> `pages = scheduler` combined with `content = dynamic` is accepted by the
> configuration but does nothing for content: the lifetime is then `null` and the
> scheduler hands content transitions to the dynamic strategy, whose
> `processTransition()` is a no-op.
> Content transitions are silently dropped in that combination.

## Time harmonization {#performance-strategies-harmonization}

Harmonization is a **data change**, not runtime behavior.
`temporalcache:harmonize` (and the equivalent backend action) rewrite the stored
`starttime`/`endtime` values of records, moving each to its nearest configured slot.
Fewer distinct transition moments means fewer cache invalidations, whatever the scoping and
timing strategy.

> [!IMPORTANT]
> Enabling `harmonization.enabled` on its own changes no cache behavior.
> It unlocks the harmonize command and the backend analysis; the reduction only happens
> once the command has actually written the rounded values.
> Those values are the editors' publication times, so agree the change with them first —
> and preview it with `temporalcache:harmonize --dry-run`, since the command writes to
> the database by default.

**config/system/additional.php**

```php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['harmonization'] = [
    'enabled' => true,
    'slots' => '00:00,06:00,12:00,18:00',
    'tolerance' => 3600,
];
```

### Slots and tolerance {#performance-strategies-harmonization-slots}

Slots are a single comma-separated string of `HH:MM` or `H:MM` values.
Each timestamp is moved to its *nearest* slot, and only when the distance to that slot is at
most `harmonization.tolerance` seconds.

```text
Slot at 12:00:00, tolerance = 300 seconds

11:56:00 → 4 minutes away  → shifted to 12:00:00
12:03:00 → 3 minutes away  → shifted to 12:00:00
11:50:00 → 10 minutes away → beyond tolerance, stays 11:50:00
```

The tolerance is the maximum shift the rewrite may apply, not a threshold below which
nothing happens.
A small tolerance keeps publication times close to what editors entered and harmonizes few
records; the default `3600` harmonizes anything within an hour of a slot.

> [!WARNING]
> `harmonization.tolerance = 0` shifts nothing except timestamps that already sit
> exactly on a slot.
> It does not mean "no limit".

### Effect {#performance-strategies-harmonization-example}

```text
Before harmonize, slots 00:00,06:00,12:00,18:00, tolerance 3600:

  Article 1: starttime 05:43  → within an hour of 06:00 → rewritten to 06:00
  Article 2: starttime 06:18  → within an hour of 06:00 → rewritten to 06:00
  Article 3: starttime 11:27  → within an hour of 12:00 → rewritten to 12:00
  Article 4: starttime 12:31  → within an hour of 12:00 → rewritten to 12:00

Four distinct transition moments become two.
```

Records further from a slot than the tolerance keep their time and keep their own
transition moment, so the reduction depends on how the existing publication times are
distributed — not on the number of slots alone.

## Combining the three {#performance-strategies-combining}

**config/system/additional.php — narrow invalidation, no query cost per request**

```php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache'] = [
    'scoping' => [
        'strategy' => 'per-content',
        'use_refindex' => true,
    ],
    'timing' => [
        'strategy' => 'scheduler',
    ],
    'harmonization' => [
        'enabled' => true,
        'slots' => '00:00,06:00,12:00,18:00',
        'tolerance' => 3600,
    ],
];
```

What this configuration gives, and what it costs:

-   No queries during page generation, because the lifetime calculation is skipped entirely.
-   Invalidation limited to the pages the refindex reports for a transitioning element.
-   Up to one scheduler interval of delay before a transition takes effect.
-   Menus on unaffected pages are not refreshed by a page transition, because the flush tag is
    that page's own — this is the price of leaving `global` scoping.
-   A scheduler run that loads all temporal records on the site, whether or not any of them
    transitioned.

Requires the Scheduler task to be registered and cron to be running.
If either is missing, nothing invalidates anything.

## Next steps {#performance-strategies-next-steps}

-   [Limitations](Limitations.html#performance-limitations) — what the approach cannot do
-   [Decision guide](DecisionGuide.html#decision-guide) — which configuration matches which site
-   [Configuration](../Configuration/Index.html#configuration) — every option in detail
