Temporal Cache Management 

Extension key

nr_temporal_cache

Package name

netresearch/nr-temporal-cache

Version

0.9

Language

en

Author

Netresearch DTT GmbH

License

This document is published under the Creative Commons BY 4.0 license.

Rendered

Fri, 04 Sep 2026 06:45:30 +0000


Automatic cache invalidation for time-based content in TYPO3.

Addresses TYPO3 Forge Issue #14277: Menus and content with starttime/endtime update automatically when time passes, without manual cache clearing.


Documentation 

📘 Introduction 

Get started with the Temporal Cache extension. Understand what problem it solves, how it works, and whether it's right for your TYPO3 site.

⚡ Performance Considerations 

CRITICAL: Read this before production deployment. Understand performance implications, site-wide cache synchronization, and optimization strategies.

🔧 Installation 

Complete installation guide for TYPO3 v12.4 LTS, v13 and v14 including Composer setup, extension activation, and verification steps.

🎯 Configuration 

Configure optimization strategies, timing modes, and monitoring to match your site's requirements and infrastructure.

🖥️ Backend Module 

Monitor temporal transitions, analyze cache performance, and validate extension functionality through the TYPO3 backend interface.

📊 Reports Module 

Track transition history, cache hit rates, and system health through integrated TYPO3 Reports module analytics.

🏗️ Architecture 

Deep dive into root cause analysis, the implementation approach, and how the extension addresses the temporal content problem.

🔮 Approach & Limits 

Why the extension works the way it does, what the approach cannot do, and what a solution inside TYPO3 core would have to provide.

Introduction 

The problem 

TYPO3 invalidates a page cache entry when the data behind it changes, or when somebody flushes a cache tag. Neither happens when time simply passes.

A page or a content element with starttime or endtime changes its visibility at a fixed moment without any record being edited. The cached output still holds the visibility snapshot taken when it was rendered, and it keeps holding it until its lifetime runs out or an editor clears the cache by hand.

This is the subject of TYPO3 Forge issue #14277, which is still open.

Symptoms 

Expiring content
A page with an endtime in the past stays in cached menus.
Scheduled content
A page with a starttime that has arrived does not appear in cached menus.
Content elements
An element with starttime/endtime does not appear or disappear in cached page output.
Anything rendered from a cached page
Sitemaps, breadcrumbs and listings inherit the same stale snapshot.

What this extension does 

The extension registers a listener on TYPO3\CMS\Frontend\Event\ModifyCacheLifetimeForPageEvent. When TYPO3 writes a page cache entry, the listener asks the configured strategies for the next starttime/endtime transition and shortens the entry's lifetime so it ends there. The page is then regenerated at that moment with the correct visibility.

An alternative mode replaces the shortened lifetime with a Scheduler task that flushes the affected cache tags in the background. Which of the two runs is a configuration choice; see Configuration.

Default behavior and its cost 

That is a deliberate default: it is the safest setting and needs no configuration. It is also the most expensive one, and on a site with frequent transitions it can flatten the cache hit ratio.

The extension ships two ways to narrow it:

  • scoping.strategy = per-page shortens a page's lifetime for content on that page only. Page transitions are still watched site-wide, because a page appearing or disappearing changes menus everywhere.
  • timing.strategy = scheduler (with per-page or per-content scoping) stops shortening lifetimes altogether and flushes individual pageId_* tags from a background task instead.

Read Performance considerations before deploying to a site where the cache hit ratio matters, and What each scoping strategy does for what each strategy actually covers.

Status 

The approach itself is a workaround. TYPO3's cache API has no absolute expiration timestamp, so the extension can only approximate one by shortening relative lifetimes or by flushing tags from a scheduled task. A solution inside TYPO3 core would not need either. See Approach, limits and a core solution for what such a solution would look like and what would change here.

Requirements 

Requirement Value
TYPO3 ^12.4 || ^13.0 || ^14.0
PHP ^8.1
Required TYPO3 extensions scheduler, reports
License GPL-2.0-or-later

Quick start 

Install
composer require netresearch/nr-temporal-cache
Copied!

Run the database analyzer afterwards so the indexes from ext_tables.sql are created, then confirm the installation:

Verify
vendor/bin/typo3 temporalcache:verify
Copied!

No configuration is required. The extension is active as soon as it is installed, with global scoping and dynamic timing.

Next steps 

Performance considerations 

Overview 

The extension buys correct temporal behavior by making page cache entries expire earlier than they otherwise would, or by flushing them from a background task. Both cost cache hits. How many depends entirely on which scoping and timing strategy is configured, and the default is the most expensive combination.

This chapter describes what each configuration actually does, so the cost can be measured on a specific site. It contains no benchmark figures: none have been measured for this extension, and numbers from another site would not transfer.

The model in one table 

Scoping and timing are independent, and scoping answers two different questions depending on which timing strategy reads it.

Scoping with dynamic timing (shortens lifetimes) with scheduler timing (flushes tags)
global Every entry expires at the earliest transition site-wide. Flushes the pages tag: the entire page cache, once per transition.
per-page An entry expires at the earlier of: the next pages transition site-wide, or the next transition of a content element on that page. Flushes pageId_<uid> for a page, pageId_<pid> for a content element.
per-content Same as global — this strategy does not narrow lifetimes. Flushes one pageId_* tag per page that sys_refindex reports for the element.

Read the table before choosing a strategy. Two combinations are commonly misread:

  • per-content + dynamic narrows nothing. Its precision lives in the flush tags, which dynamic timing never reads. Configuring it without switching timing gives global behavior plus refindex code that never runs.
  • per-page or per-content + scheduler flushes only the tags named above. A page transition then refreshes that page, not the menus on every other page. Only global scoping refreshes those.

hybrid timing picks per record type: timing.hybrid.pages decides the lifetime calculation and page transitions, timing.hybrid.content decides content transitions.

Where the cost sits 

Query cost with dynamic timing
Every page cache write runs two MIN() queries per monitored table — four with the default pages and tt_content, two more for every table another extension registers. Each query is a single indexed aggregate returning one integer. One request carries one workspace and one language, so the query count does not grow with the number of configured languages.
Cache hit ratio
This is the cost that matters, and it is a property of the site, not of the extension: how often transitions occur, how many pages exist, and how much traffic arrives between two transitions.
Simultaneous expiry
With global scoping every entry carries the same expiry timestamp, so they all miss at once. Behind a CDN or reverse proxy that miss propagates upstream. per-page scoping spreads the timestamps for content transitions; page transitions still land on all entries at once.
Query cost with scheduler timing
Zero during page generation. The cost moves into the scheduled task, which is heavier than the lookup it replaces: each run loads every record carrying a starttime or endtime from every monitored table into PHP — one query per table, no time restriction in SQL — and then filters the run's time window in PHP. Its cost therefore scales with the total amount of temporal content on the site, not with the number of transitions in the window. Each transition that does fall in the window costs one flushByTag() per tag the scoping strategy names.

Chapters 

⚡ Optimization strategies 

What scoping, timing and harmonization each change, and how to combine them.

⚠️ Limitations 

What the approach cannot do, and the failure modes to plan for.

🎯 Decision guide 

Which configuration matches which shape of site, and what to measure before deciding.

🔄 Alternative approaches 

Solving temporal content without this extension: uncached menus, ESI, client-side loading, scheduled clearing.

❓ Frequently asked questions 

Cache synchronization, CDNs, workspaces, and when not to use the extension.

Optimization strategies 

Three settings change how much cache the extension costs:

  1. Scoping — which records a lookup covers and which tags a transition flushes
  2. Timing — whether invalidation happens through a shortened lifetime or a background task
  3. 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 for the full matrix before reading on.

Scoping strategies 

Global (default) 

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['scoping']['strategy'] = 'global';
Copied!
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'] — the tag every page cache entry carries, so a transition flushes the whole page cache.
Trade-off
Nothing to configure and nothing can be missed. Every transition anywhere costs the whole page cache.

Per-page 

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['scoping']['strategy'] = 'per-page';
Copied!
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 

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['scoping'] = [
    'strategy' => 'per-content',
    'use_refindex' => true,
];
Copied!
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.
Trade-off
The most precise invalidation available, but only along the flush-tag path.

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
vendor/bin/typo3 referenceindex:update
Copied!

Timing strategies 

Dynamic (default) 

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['timing']['strategy'] = 'dynamic';
Copied!

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 

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['timing']['strategy'] = 'scheduler';
Copied!

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.
  2. Let cron run the Scheduler.
Crontab
* * * * * php /path/to/typo3/vendor/bin/typo3 scheduler:run
Copied!

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.

Hybrid 

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['timing'] = [
    'strategy' => 'hybrid',
    'hybrid' => [
        'pages' => 'dynamic',
        'content' => 'scheduler',
    ],
];
Copied!

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.

Time 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.

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['harmonization'] = [
    'enabled' => true,
    'slots' => '00:00,06:00,12:00,18:00',
    'tolerance' => 3600,
];
Copied!

Slots and tolerance 

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.

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
Copied!

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.

Effect 

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.
Copied!

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 

config/system/additional.php — narrow invalidation, no query cost per request
$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,
    ],
];
Copied!

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 

Limitations 

This chapter lists what the approach cannot do and which failure modes to plan for. The model in one table has the matrix of what each configuration does; this chapter assumes it.

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.

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.

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.
Copied!

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.
Apache
<IfModule mod_headers.c>
    Header set Cache-Control "public, max-age=3600, stale-while-revalidate=300"
</IfModule>
Copied!
Varnish VCL
sub vcl_backend_response {
    set beresp.grace = 5m;
}
Copied!
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;
Copied!
  • Warm the cache before the transition, using the transition times temporalcache:list reports.

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.

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
Copied!

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 

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

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.

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.

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_<uid> for a page and pageId_<pid> 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.

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.

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.

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:

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.

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.

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.

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 Custom cache lifetime logic.

Next steps 

Decision guide 

Four questions that decide the configuration 

Where is the temporal content?
Only in the pages table, so only menus and the page tree are affected? Or in tt_content and other records too? per-page scoping only helps when content transitions outnumber page transitions: page transitions stay site-wide in every strategy.
Is content reused across pages?
If elements are placed on one page each, per-page scoping is accurate. If CONTENT or RECORDS cObjects pull elements onto other pages, only per-content scoping resolves those references — and only for flush tags, which means scheduler or hybrid timing.
How far apart are transitions?
With dynamic timing the effective page cache lifetime is the gap to the next transition in scope. If that gap is routinely shorter than the interval in which a page would otherwise be requested twice, the page cache is doing little work.
Is a delay acceptable?
scheduler timing trades exactness for zero per-request cost. Content appears or disappears up to one scheduler interval late.

Configurations 

Default: global scoping, dynamic timing 

config/system/additional.php — this is what an unconfigured install does
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache'] = [
    'scoping' => ['strategy' => 'global'],
    'timing' => ['strategy' => 'dynamic'],
];
Copied!

Fits a site where transitions are rare and the page count is small enough that regenerating everything is cheap. Nothing can be missed and nothing has to be set up.

The cost is exact and predictable: every transition anywhere expires the whole page cache. If transitions are frequent, this is the configuration to move away from first.

Narrow the lifetime: per-page scoping, dynamic timing 

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache'] = [
    'scoping' => ['strategy' => 'per-page'],
    'timing' => ['strategy' => 'dynamic'],
];
Copied!

Fits a site whose temporal content is mostly content elements sitting on the page they belong to. A scheduled element then shortens only its own page's lifetime.

What it does not change: page transitions still shorten every page's lifetime, because a page entering or leaving the tree changes menus everywhere. On a site whose temporal content is mostly pages, this configuration behaves close to the default.

What it can miss: an element embedded onto another page through CONTENT/RECORDS. That page keeps its long lifetime through the element's transition.

Remove per-request cost: scheduler timing 

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache'] = [
    'scoping' => ['strategy' => 'per-content', 'use_refindex' => true],
    'timing' => ['strategy' => 'scheduler'],
];
Copied!

Fits a site where the page cache has to keep its normal lifetime and a delay of one scheduler interval is acceptable. Page generation then runs no extra query at all, and invalidation is limited to the pages the reference index reports.

Requires the Scheduler task to be registered and cron to run it; without both, nothing is invalidated. Requires sys_refindex to be current, otherwise the strategy silently falls back to the element's own page.

Consider before choosing it:

  • A page transition flushes only that page's tag, so menus elsewhere are not refreshed. If correct menus matter more than cache hits, keep global scoping and accept the full flush.
  • The task loads every temporal record on the site on each run.
  • Transitions on translated records are not processed: the task runs against the live workspace and the default language only.

Split the two: hybrid timing 

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache'] = [
    'scoping' => ['strategy' => 'per-content', 'use_refindex' => true],
    'timing' => [
        'strategy' => 'hybrid',
        'hybrid' => [
            'pages' => 'dynamic',
            'content' => 'scheduler',
        ],
    ],
];
Copied!

Fits a site that needs menus to be exact but can tolerate a delay on content elements. Page transitions keep shortening lifetimes; content transitions are handled by the task.

Add harmonization when publication times are scattered 

Harmonization helps whichever strategy is configured, because it reduces the number of distinct transition moments in the data. It is a rewrite of editorial starttime/endtime values, so it needs the editors' agreement, and its effect depends on how close the existing times already are to the chosen slots. See Time harmonization.

When not to use the extension 

No record uses starttime or endtime
Every lookup returns null and the lifetime falls back to advanced.default_max_lifetime. The queries still run. There is nothing to gain.
Correct menus everywhere are required and the full flush is unaffordable
The two are in tension: only global scoping refreshes menus on unaffected pages, and only the narrower strategies keep the cache. No configuration resolves that; see Alternative approaches for approaches that take menus out of the page cache entirely.
Manual clearing is already reliable in practice
Then the extension only adds moving parts.

What to measure 

Before deploying, on a copy of the production data:

How much temporal content exists, and when it transitions
vendor/bin/typo3 temporalcache:analyze
vendor/bin/typo3 temporalcache:list
Copied!
Confirm the indexes exist before measuring query cost
vendor/bin/typo3 temporalcache:verify
Copied!

Then measure, with the extension installed and again without it:

  • The page cache hit ratio over a period covering several transitions.
  • Page generation time on a cache miss, which tells you the cost of a lookup on your data volume.
  • What arrives at the origin when a transition passes, if a CDN or reverse proxy is in front.

Enable advanced.debug_logging while doing this: the listener then logs the lifetime it set, the cap it applied and both strategy names for every cache write.

Next steps 

Alternative approaches 

This extension is not the only way to keep time-based content current, and for some sites it is not the best one. The approaches below solve the same problem outside the page cache lifetime.

None of them is provided by this extension; they are listed so the choice can be an informed one.

The trade-off in one line each 

Approach What it costs What it buys
This extension Cache hits: entries expire earlier, or are flushed by a task. Correct output for everything on the page, with no template changes.
Uncached menu (USER_INT) CPU on every request, for the menu only. The page cannot be cached whole at the edge. Exact menus, no cache churn for the rest of the page.
SSI / ESI Infrastructure complexity, and a server or CDN that supports it. Exact fragments with the rest of the page fully cached, including at the edge.
Client-side loading A request after page load; the fragment is invisible to crawlers and needs an accessible fallback. The page HTML stays fully cacheable and static.
Scheduled cache clearing Cache hits at fixed intervals, whether or not anything changed. Almost no setup.
Manual clearing Editorial attention, and it will be forgotten. Nothing to install.

Uncached menus (USER_INT) 

Render the navigation outside the page cache so it is rebuilt on every request.

TypoScript
lib.mainMenu = USER_INT
lib.mainMenu {
    userFunc = MyVendor\MyExtension\Menu\MenuProcessor->render
}
Copied!

Fits when the temporal content is only in menus. The rest of the page keeps its normal cache lifetime and nothing has to expire early, which is the opposite trade to this extension: a steady per-request cost instead of periodic cache loss.

Does not help with temporal content in the page body, and a page containing a USER_INT cannot be delivered from a reverse proxy as a whole.

SSI and ESI 

Keep the page cached and let the web server or CDN assemble an uncached fragment into it at delivery time.

Template, ESI
<div class="navigation">
    <esi:include src="/menu-fragment" />
</div>
Copied!
Varnish VCL
sub vcl_recv {
    if (req.url ~ "^/menu-fragment") {
        return (pass);
    }
}
Copied!
Apache, SSI
<IfModule mod_include.c>
    Options +Includes
    AddOutputFilter INCLUDES .html
</IfModule>
Copied!

Fits a site that already runs Varnish or a CDN with ESI support. It is the only approach here that keeps both the page fully cached at the edge and the fragment exact.

The cost is operational: another moving part in the delivery chain, and debugging that spans TYPO3 and the proxy.

Client-side loading 

Ship the page without the time-sensitive fragment and fetch it after load.

Frontend
const response = await fetch('/api/menu');
const items = await response.json();

const list = document.querySelector('.navigation ul');
list.replaceChildren(...items.map(item => {
    const link = document.createElement('a');
    link.href = item.url;
    link.textContent = item.title;

    const entry = document.createElement('li');
    entry.append(link);

    return entry;
}));
Copied!

Fits an application-style frontend that is already doing this for other data.

For navigation it is usually the wrong choice: search engines and assistive technology need the links in the delivered HTML, and a fragment that appears after load is a layout shift. If it is used, the server-rendered fallback has to be correct on its own.

Scheduled cache clearing 

Crontab
0 * * * * /path/to/typo3/vendor/bin/typo3 cache:flush
Copied!

Fits a site with a fixed editorial rhythm — everything publishes at 09:00, 12:00 and 17:00 — where clearing shortly after those times is enough.

It does not solve the underlying problem: between two runs the output is stale, and every run discards the whole cache whether or not a transition happened. Compared to this extension with global scoping, it is the same full flush on a fixed schedule instead of on an actual transition.

Combining harmonization with the extension gets a similar grouping effect without discarding caches that nothing invalidated; see Time harmonization.

Manual clearing 

The editor clears the cache after the scheduled moment has passed.

This is the status quo the extension exists to replace. It is listed for completeness: it works, it costs nothing to set up, and it fails the first time somebody is on holiday.

Choosing between them 

Where is the temporal content?
Only in menus → an uncached menu or ESI keeps the page cache intact. In the page body → this extension, or client-side loading for that fragment.
What is in front of TYPO3?
A CDN or Varnish with ESI support makes ESI the strongest option. Without one, ESI is not available and an uncached menu costs origin CPU on every request.
How exact does it have to be?
Exact to the second → this extension with dynamic timing, an uncached menu, or ESI. Within a few minutes → this extension with scheduler timing, or scheduled clearing.
Are the approaches mutually exclusive?
No. An uncached menu plus this extension for content elements is a reasonable split: the menu never goes stale, and the extension only has to watch tt_content.

Next steps 

Frequently asked questions 

Why does my entire site cache expire when one page has a future starttime? 

Because the default scoping strategy is global. Its transition lookup covers every monitored table site-wide and ignores the page id, so every page cache entry written in that second gets the same shortened lifetime.

Switching to per-page narrows it — but only for content elements:

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['scoping']['strategy'] = 'per-page';
Copied!

A transition in the pages table still shortens every page's lifetime, in every strategy, because a page entering or leaving the tree changes menus everywhere.

Can I disable this for specific page trees? 

Not through configuration. The settings are global to the installation; there is no page-tree, doktype or content-type filter.

The workaround is a second listener on the same event, ordered after this extension's, that overrides the lifetime for the pages it should not apply to:

EXT:my_extension/Classes/EventListener/ConditionalTemporalCache.php
namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Frontend\Event\ModifyCacheLifetimeForPageEvent;

final class ConditionalTemporalCache
{
    /**
     * @param int[] $excludedPageIds pages that keep the long lifetime
     */
    public function __construct(private readonly array $excludedPageIds = [])
    {
    }

    public function __invoke(ModifyCacheLifetimeForPageEvent $event): void
    {
        if (\in_array($event->getPageId(), $this->excludedPageIds, true)) {
            $event->setCacheLifetime(86400);
        }
    }
}
Copied!
EXT:my_extension/Configuration/Services.yaml
services:
  MyVendor\MyExtension\EventListener\ConditionalTemporalCache:
    tags:
      - name: event.listener
        identifier: 'my-extension/conditional-temporal-cache'
        event: TYPO3\CMS\Frontend\Event\ModifyCacheLifetimeForPageEvent
        after: 'temporal-cache/modify-cache-lifetime'
Copied!

Will this work with a CDN or Varnish? 

Yes, with one caveat. A CDN honors the Cache-Control window it is given, so a shortened page cache lifetime propagates to the edge. With global scoping every entry carries the same expiry, so the edge misses arrive together and reach the origin as one wave.

The mitigations are the usual ones — serve stale while revalidating, rate-limit the origin, warm the cache ahead of a known transition — plus the extension-side option of moving to scheduler timing, which stops shortening lifetimes altogether. See Synchronized expiry with global scoping.

Does this affect backend performance? 

The cache lifetime listener runs on frontend page cache writes only, so ordinary backend editing is untouched.

Three parts of the extension do run in the backend, on demand: the backend module, the Reports module status provider, and the console commands. The backend module and the harmonization analysis load temporal records to build their figures, so they get slower as the amount of temporal content grows.

What if I do not use temporal content at all? 

Every lookup returns null and the lifetime falls back to advanced.default_max_lifetime (default 86400), so behavior is unchanged. The queries still run on every page cache write with dynamic timing.

There is no benefit in that case — uninstall it.

How do I see what the extension is doing? 

Turn on advanced.debug_logging. The listener then logs, for every page cache write it modifies: the lifetime it set, the uncapped value, the cap and where the cap came from, and the names of both active strategies.

config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['advanced']['debug_logging'] = true;
Copied!

The same flag makes SchedulerTimingStrategy log each flush with the tags it flushed, and makes the scheduler task log its run window.

To see the data rather than the decisions:

vendor/bin/typo3 temporalcache:analyze   # counts and statistics
vendor/bin/typo3 temporalcache:list      # every temporal record and its next transition
vendor/bin/typo3 temporalcache:verify    # indexes and configuration
Copied!

Can I combine this with cache warming? 

Yes, and it is worth doing with global scoping, where all entries expire at the same known moment. temporalcache:list reports the next transition per record, so a warming run can be scheduled shortly after it.

The extension ships no warming itself and integrates with no particular warming extension; anything that requests pages after the transition works.

What happens under load when the cache expires? 

With global scoping, every page cache entry expires at the same second, so every subsequent request is a miss until the entries are rebuilt. Under load that arrives at the origin as a single burst.

Four levers, in rough order of effectiveness:

  • scheduler timing — nothing expires by time.
  • per-page scoping — content transitions stagger; page transitions do not.
  • Stale-while-revalidate at the edge, so the burst is absorbed.
  • Cache warming timed to the known transition.

How does this work with workspaces? 

With dynamic timing, correctly and without configuration. The strategies read the workspace id from the Context API and pass it into every query; a live request and a workspace preview therefore resolve different transitions and get different lifetimes.

Can I combine this with my own cache tags? 

Yes. The extension sets the page cache lifetime and, under scheduler timing, flushes pages or pageId_* tags. It does not interfere with tags your own code adds or flushes.

If your listener also sets a lifetime, order it after temporal-cache/modify-cache-lifetime and combine the two values with min() rather than overwriting.

What does harmonization cost? 

Nothing at runtime — it changes no code path. temporalcache:harmonize rewrites the stored starttime/endtime values once, and from then on the same transition lookups simply find fewer distinct moments.

The real cost is editorial: publication times move to the nearest slot within the configured tolerance. Preview with --dry-run before running it, because the command writes by default.

Does the query cost multiply with the number of languages? 

No. One frontend request carries one language, and the language id from the Context API goes into the query as a single sys_language_uid condition. A ten-language site runs the same number of queries per cache write as a single-language site — each language just maintains its own cache entries and its own transitions.

The number of queries grows with the number of monitored tables, at two per table.

Next steps 

Installation 

Requirements 

Minimum, as declared in composer.json:

  • PHP 8.1 or newer
  • TYPO3 ^12.4 || ^13.0 || ^14.0
  • typo3/cms-scheduler and typo3/cms-reports, both pulled in as dependencies

Database

  • Four indexes are added through ext_tables.sql, two on pages and two on tt_content; run the database compare after installing
  • No tables and no columns are added — the extension reads the standard starttime and endtime fields

Compatibility 

The combinations below are the ones the CI matrix in .github/workflows/ci.yml builds.

TYPO3 version PHP version Status Notes
12.4+ 8.1 - 8.4 ✅ Supported PHP 8.5 is excluded from this cell in CI
13.0+ 8.2 - 8.5 ✅ Supported PHP 8.1 is excluded from this cell in CI
14.0+ 8.3 - 8.5 ✅ Supported PHP 8.1 and 8.2 are excluded from this cell in CI
11.5 ⚠️ Not supported Below the typo3/cms-core: ^12.4 requirement

Installation methods 

Method 2: TER (Extension Repository) 

  1. Go to Admin Tools → Extensions
  2. Click Get Extensions
  3. Search for nr_temporal_cache
  4. Click Import and Install
  5. Activate the extension

Method 3: Manual installation 

  1. Download from GitHub
  2. Extract to typo3conf/ext/nr_temporal_cache/ (classic mode) or packages/nr_temporal_cache/ (Composer mode)
  3. Activate in the Extension Manager
  4. Clear all caches

Configuration 

Zero configuration 

The extension works immediately after installation. It automatically:

  • Registers the PSR-14 listener for ModifyCacheLifetimeForPageEvent under the identifier temporal-cache/modify-cache-lifetime
  • Monitors the pages and tt_content tables
  • Caps the page cache lifetime at the next transition, using global scoping and dynamic timing

All twelve settings and their defaults are listed in Configuration.

Optional: Monitor custom tables 

If you have custom extension tables with starttime/endtime fields, you can register them for temporal cache monitoring using the TemporalMonitorRegistry.

Recommended: Configure in Configuration/Services.yaml (modern dependency injection):

services:
  # Register custom news table
  my_ext_news_table_registration:
    class: 'Closure'
    factory: ['@Netresearch\TemporalCache\Service\TemporalMonitorRegistry', 'registerTable']
    arguments:
      - 'tx_news_domain_model_news'
      - ['uid', 'pid', 'title', 'starttime', 'endtime', 'hidden', 'deleted', 'sys_language_uid']

  # Register custom event table
  my_ext_events_table_registration:
    class: 'Closure'
    factory: ['@Netresearch\TemporalCache\Service\TemporalMonitorRegistry', 'registerTable']
    arguments:
      - 'tx_events_domain_model_event'
      - ['uid', 'pid', 'title', 'starttime', 'endtime', 'hidden', 'deleted', 'sys_language_uid']
Copied!

Alternative: For ext_localconf.php (when DI not available):

<?php
use Netresearch\TemporalCache\Service\TemporalMonitorRegistry;
use TYPO3\CMS\Core\Utility\GeneralUtility;

// Only use makeInstance() in ext_localconf.php where DI is not yet available
$registry = GeneralUtility::makeInstance(TemporalMonitorRegistry::class);
$registry->registerTable('tx_news_domain_model_news', [
    'uid', 'pid', 'title', 'starttime', 'endtime', 'hidden', 'deleted', 'sys_language_uid'
]);
Copied!

Field requirements

registerTable() rejects a registration that misses one of these:

  • uid
  • starttime
  • endtime

Recommended in addition, because the queries and the backend module use them:

  • pid — parent page id
  • hidden and deleted — the transition queries exclude records whose TCA delete and enablecolumns.disabled fields are set
  • sys_language_uid — the queries filter on the language of the context
  • a label field such as title, header or name for display

Passing an empty field list applies the default list uid, pid, title, starttime, endtime, hidden, deleted, sys_language_uid.

Default tables

pages and tt_content are monitored out of the box. Re-registering either of them throws an exception.

Each registered table adds two MIN() queries to a transition lookup — see High database load.

Optional: Adjust the maximum lifetime 

advanced.default_max_lifetime caps the cache lifetime the extension calculates, and is the lifetime used when no transition is scheduled at all.

Default: 86400 seconds (24 hours)

Configure it in Admin Tools → Extensions → nr_temporal_cache → Configure, under Default Cache Lifetime (seconds), or in PHP:

config/system/additional.php
<?php

$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['advanced']['default_max_lifetime'] = 43200;
Copied!

TypoScript config.cache_period takes precedence over this setting; see Advanced options.

Verification 

Check the setup 

vendor/bin/typo3 temporalcache:verify
Copied!

The command confirms that the indexes and the columns the queries rely on exist, and that the configured strategy names are valid. System → Reports → Temporal Cache shows the same configuration from the backend.

Test scheduled content 

  1. Create a test page:

    • Set Start to 5 minutes in the future
    • Enable In menu
    • Save
  2. Check the frontend menu — the page must not appear yet
  3. Wait until the start time and reload — the page appears without any cache being cleared by hand

The page cache lifetime is capped at the transition, so the first request after the start time regenerates the page.

Test expiring content 

  1. Create a content element with Stop 5 minutes in the future
  2. View the page — the element is visible
  3. Wait until the stop time and reload — the element is gone

Inspect what the extension calculated 

advanced.debug_logging = 1
Copied!

Each page cache generation then logs the lifetime that was written, the uncapped value, and which maximum applied. See Advanced options.

Troubleshooting 

Content does not update 

  1. Confirm the extension is loaded:

    vendor/bin/typo3 extension:list
    Copied!
  2. Confirm the timing strategy. scheduler always depends on the scheduler task, and hybrid does when at least one of timing.hybrid.pages and timing.hybrid.content is set to scheduler. In those cases confirm the task exists and cron is running it — see Scheduler task. A hybrid configuration with both rules on dynamic needs no task.
  3. Clear all caches:

    vendor/bin/typo3 cache:flush
    Copied!
  4. Work through Troubleshooting

Slow page generation 

The dynamic timing strategy runs two MIN() queries per monitored table on every page cache generation. Confirm the indexes exist:

SHOW INDEX FROM pages WHERE Key_name LIKE 'idx_temporalcache%';
SHOW INDEX FROM tt_content WHERE Key_name LIKE 'idx_temporalcache%';
Copied!

If they are missing, run the database compare — the definitions ship with the extension:

vendor/bin/typo3 extension:setup
Copied!

To log the extension's own messages into a separate file:

config/system/additional.php
<?php

$GLOBALS['TYPO3_CONF_VARS']['LOG']['Netresearch']['TemporalCache']['writerConfiguration'] = [
    \TYPO3\CMS\Core\Log\LogLevel::DEBUG => [
        \TYPO3\CMS\Core\Log\Writer\FileWriter::class => [
            'logFile' => 'typo3temp/var/log/temporal_cache.log',
        ],
    ],
];
Copied!

Workspace and language 

Every transition query resolves the workspace and the language from the Context API and filters on both. A transition scheduled in another language therefore does not shorten the lifetime of the page you are looking at.

Uninstallation 

The extension adds no tables and no columns. It does add four indexes to pages and tt_content (idx_temporalcache_starttime and idx_temporalcache_endtime, see ext_tables.sql); remove them in Admin Tools → Maintenance → Analyze Database Structure after uninstalling if you do not want to keep them.

composer remove netresearch/nr-temporal-cache
vendor/bin/typo3 cache:flush
Copied!

TYPO3 then reverts to its default behaviour: temporal content becomes visible when the page cache happens to expire.

Next steps 

Configuration 

Reference for the twelve extension configuration settings of nr_temporal_cache.

All of them are optional. With no configuration the extension uses global scoping and dynamic timing, and harmonization is off.

Chapters 

🎯 Optimization strategies 

Scoping, timing and harmonization settings, and how the three interact.

Covers: scoping.strategy, scoping.use_refindex, timing.strategy, timing.hybrid.pages, timing.hybrid.content, harmonization.enabled, harmonization.slots, harmonization.tolerance, harmonization.auto_round

⚙️ Advanced options 

Cache lifetime cap, debug logging, and the state of the scheduler task.

Covers: advanced.default_max_lifetime, advanced.debug_logging, Scheduler task

📋 Examples & presets 

The presets the backend wizard offers, and worked configurations with what each of them changes.

🔧 Troubleshooting 

Cache not updating, high database load, harmonization doing nothing, settings that appear to be ignored.

All settings at a glance 

Defaults as implemented in NetresearchTemporalCacheConfigurationExtensionConfiguration.

Setting Type Default Accepted values
scoping.strategy string global global, per-page, per-content
scoping.use_refindex boolean true 0, 1
timing.strategy string dynamic dynamic, scheduler, hybrid
timing.hybrid.pages string dynamic dynamic, scheduler
timing.hybrid.content string scheduler dynamic, scheduler
harmonization.enabled boolean false 0, 1
harmonization.slots string 00:00,06:00,12:00,18:00 comma-separated HH:MM or H:MM
harmonization.tolerance integer 3600 seconds; 0 harmonizes nothing
harmonization.auto_round boolean false 0, 1
advanced.default_max_lifetime integer 86400 seconds greater than 0
advanced.debug_logging boolean false 0, 1

Where to configure 

Extension Manager 

1. Admin Tools → Extensions
2. Find "nr_temporal_cache"
3. Click the "Configure" icon
4. Adjust the settings, grouped by scoping, timing, harmonization, advanced
5. Save
Copied!

The form is generated from ext_conf_template.txt, and the values are stored in config/system/settings.php.

PHP configuration 

config/system/additional.php
<?php

$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache'] = [
    'scoping' => [
        'strategy' => 'per-page',
        'use_refindex' => true,
    ],
    'timing' => [
        'strategy' => 'dynamic',
    ],
    'harmonization' => [
        'enabled' => true,
        'slots' => '00:00,06:00,12:00,18:00',
        'tolerance' => 3600,
    ],
];
Copied!

additional.php is read after config/system/settings.php, so it wins over the Extension Manager.

Backend module wizard 

Tools → Temporal Cache → Wizard walks through five steps — welcome, analysis, presets, custom, summary — showing statistics for the current site and recommending settings.

Next steps 

Optimization strategies 

Reference for the scoping, timing and harmonization settings. Each setting is listed with the type, the default and the behaviour the extension code derives from it.

For guidance on which combination suits which site, see Optimization strategies.

How the settings combine 

The scoping strategy answers two questions, and the timing strategy decides which of the two answers is ever used:

Cache lifetime
ScopingStrategyInterface::getNextTransition() returns the next transition timestamp. The event listener caps the page cache lifetime at that timestamp. Only the dynamic timing strategy — and hybrid when its page rule is dynamic — asks for it.
Cache tags
ScopingStrategyInterface::getCacheTagsToFlush() returns the tags a transition flushes. Only the scheduler task calls TimingStrategyInterface::processTransition(), so these tags take effect with the scheduler and hybrid timing strategies. DynamicTimingStrategy::processTransition() is empty.

A scoping strategy whose benefit lies in its tags therefore has no effect while timing.strategy = dynamic.

Scoping strategy 

Controls which caches are invalidated when temporal transitions occur.

scoping.strategy

scoping.strategy
type

string

Default

global

Path

$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['scoping']['strategy']

Selects the scoping strategy by its getName(). Accepted values are global, per-page and per-content. A value matching no registered strategy activates the tagged strategy with the highest priority, which is global.

global
Next transition: the earliest transition in any monitored table, for the current workspace and language. Cache tags: pages — every page cache is flushed.
per-page
Next transition: the earlier of the next transition in the pages table site-wide and the next content transition on the page being rendered. Page transitions stay site-wide because a page appearing or disappearing changes menus everywhere. Without a page id, for example on the command line, the site-wide transition is used. Cache tags: pageId_<uid> for a page record, pageId_<pid> for a content element.
per-content
Next transition: the site-wide transition, the same value global returns. Content can be referenced onto arbitrary pages, so narrowing the lifetime per page would risk serving stale embedded content. Cache tags: pageId_<uid> for a page record; for a content element, one tag per page that references it, resolved through sys_refindex.

The per-content precision lives entirely in the tags, so this strategy changes nothing while timing.strategy = dynamic. Combine it with scheduler or hybrid timing.

Example -------

# Extension Manager configuration
scoping.strategy = per-content
Copied!

scoping.use_refindex

scoping.use_refindex
type

boolean

Default

true

Path

$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['scoping']['use_refindex']

Read by the per-content scoping strategy only. The other two strategies ignore it.

When enabled:

PerContentScopingStrategy asks RefindexService for every page that references the content element and returns one cache tag per page.

When disabled:

The lookup is skipped and the strategy flushes the content element's own page (pid) only, which is what per-page does.

The same fallback to pid applies when the reference index lookup returns no pages or throws, so a stale sys_refindex degrades the result instead of dropping the invalidation. Keep the reference index current with vendor/bin/typo3 referenceindex:update.

Example -------

# Extension Manager configuration
scoping.use_refindex = 1
Copied!

Timing strategy 

Controls when the extension checks for temporal transitions.

timing.strategy

timing.strategy
type

string

Default

dynamic

Path

$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['timing']['strategy']

Selects the timing strategy by its getName(). Accepted values are dynamic, scheduler and hybrid. A value matching no registered strategy activates the tagged strategy with the highest priority, which is dynamic.

dynamic
Calculates a cache lifetime on every page cache generation and caps the page cache at the next transition. With no transition ahead it returns advanced.default_max_lifetime; a transition already in the past yields 60 seconds. Transitions are not processed separately — the cache simply expires.
scheduler
Returns no lifetime, so the extension leaves the page cache lifetime untouched and TYPO3's own cache period applies. Invalidation happens when the scheduler task processes a transition and flushes the scoping strategy's cache tags. Requires the scheduler task, see Scheduler task.
hybrid
Delegates per content type to the dynamic or the scheduler strategy, configured through timing.hybrid.pages and timing.hybrid.content.

Example -------

# Extension Manager configuration

# Flush through the scheduler task
timing.strategy = scheduler

# Or split the two content types
timing.strategy = hybrid
timing.hybrid.pages = dynamic
timing.hybrid.content = scheduler
Copied!

timing.hybrid.pages

timing.hybrid.pages
type

string

Default

dynamic

Path

$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['timing']['hybrid']['pages']

Rule for records in the pages table, whose content type is page. Accepted values are dynamic and scheduler; any other value falls back to dynamic. Only read when timing.strategy = hybrid.

This rule does double duty. Besides routing page transitions, it is the rule HybridTimingStrategy::getCacheLifetime() consults on every page cache generation, because at that point the individual content elements of the page are not known. Leaving it at dynamic therefore keeps the lifetime calculation running for every cached page; setting it to scheduler removes the lifetime calculation for the whole site.

Example -------

timing.strategy = hybrid
timing.hybrid.pages = dynamic
Copied!

timing.hybrid.content

timing.hybrid.content
type

string

Default

scheduler

Path

$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['timing']['hybrid']['content']

Rule for records in every monitored table other than pages, whose content type is content. Accepted values are dynamic and scheduler; any other value falls back to dynamic. Only read when timing.strategy = hybrid.

This rule applies to transition processing only. The cache lifetime always follows timing.hybrid.pages, so setting this rule to dynamic means content transitions are neither processed by the scheduler task nor reflected in a lifetime of their own.

Example -------

timing.strategy = hybrid
timing.hybrid.content = scheduler
Copied!

Time harmonization 

Rounds transition timestamps to fixed time slots so that several transitions share one cache flush.

harmonization.enabled

harmonization.enabled
type

boolean

Default

false

Path

$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['harmonization']['enabled']

Master switch for HarmonizationService.

When disabled:

harmonizeTimestamp() returns every timestamp unchanged, the Content tab of the backend module hides its harmonization column, and the harmonize AJAX endpoint refuses the request.

When enabled:

Timestamps are moved to the nearest configured slot, subject to harmonization.tolerance. Harmonization is applied where it is invoked — by the Harmonize selected action in the backend module and by the vendor/bin/typo3 temporalcache:harmonize command, both of which write starttime and endtime back to the record. It does not silently rewrite records that editors save.

Example -------

harmonization.enabled = 1
Copied!

harmonization.slots

harmonization.slots
type

string

Default

00:00,06:00,12:00,18:00

Path

$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['harmonization']['slots']

Comma-separated list of times of day on a 24-hour clock. Both HH:MM and a single-digit hour (H:MM) are accepted, so 8:00 and 08:00 are equivalent. Surrounding whitespace is trimmed and the list is sorted internally. An entry that matches neither form, or whose hours exceed 23 or minutes exceed 59, is dropped without an error; if that leaves no slot at all, harmonization returns every timestamp unchanged.

The slots repeat every day: a timestamp is compared against the slot times of its own day, in the server timezone. The comparison does not wrap around midnight, so 23:30 is 5 hours 30 minutes from an 18:00 slot and not 30 minutes from the next day's 00:00 slot. A slot at 00:00 therefore only attracts timestamps in the early hours.

Examples --------

# Every 6 hours (4 slots per day)
harmonization.slots = 00:00,06:00,12:00,18:00

# Every 4 hours (6 slots per day)
harmonization.slots = 00:00,04:00,08:00,12:00,16:00,20:00

# Business hours only
harmonization.slots = 08:00,12:00,17:00
Copied!

harmonization.tolerance

harmonization.tolerance
type

integer

Default

3600

Path

$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['harmonization']['tolerance']

Largest shift in seconds that harmonization may apply. A timestamp is moved to its nearest slot only when the distance to that slot is at most this many seconds; anything further away is returned unchanged.

Example behaviour (slots 00:00,12:00, tolerance 3600):

11:30 → nearest slot 12:00, distance 30 min → harmonized to 12:00
12:45 → nearest slot 12:00, distance 45 min → harmonized to 12:00
10:30 → nearest slot 12:00, distance 90 min → left at 10:30
Copied!

When two slots are equally distant, the later one wins.

Examples --------

# Allow up to 1 hour shift (default)
harmonization.tolerance = 3600

# Stricter: max 30 minutes shift
harmonization.tolerance = 1800
Copied!

harmonization.auto_round

harmonization.auto_round
type

boolean

Default

false

Path

$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['harmonization']['auto_round']

Declares that harmonized times should be suggested while editing.

Example -------

harmonization.auto_round = 1
Copied!

Next steps 

Advanced options 

Cache lifetime cap, debug logging and the scheduler task.

Advanced settings 

advanced.default_max_lifetime

advanced.default_max_lifetime
type

integer

Default

86400

Path

$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['advanced']['default_max_lifetime']

Upper bound in seconds for the cache lifetime this extension calculates.

Two places read it:

  1. DynamicTimingStrategy::getCacheLifetime() returns this value when no transition is scheduled at all, and caps the calculated lifetime at it otherwise.
  2. TemporalCacheLifetime caps the value it finally writes to the event, using the hierarchy below.

Configuration hierarchy

TemporalCacheLifetime::determineMaxLifetime() takes the first value that applies:

  1. TypoScript config.cache_period, when it is set and greater than 0
  2. This setting, when it is greater than 0
  3. 86400 seconds

A value of 0 or less is therefore not a way to lift the cap — it is skipped and 86400 is used.

Prefer TypoScript for a site-wide period

setup.typoscript
config.cache_period = 43200
Copied!

TypoScript wins over this setting and applies to all other TYPO3 cache handling as well. Configure the extension setting only when temporal cache should have a different maximum than the rest of the site.

Examples --------

# Default: 24 hours
advanced.default_max_lifetime = 86400

# Shorter: 12 hours
advanced.default_max_lifetime = 43200

# Longer: 48 hours
advanced.default_max_lifetime = 172800
Copied!

With advanced.debug_logging enabled, the listener records which source it used in the max_from_typoscript, max_from_extension_config and max_lifetime keys of its log entry.

advanced.debug_logging

advanced.debug_logging
type

boolean

Default

false

Path

$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['advanced']['debug_logging']

Gates the extension's diagnostic log entries. Three call sites check it:

TemporalCacheLifetime
Debug entry per page cache generation in which a lifetime was set, with the keys lifetime, uncapped_lifetime, max_lifetime, max_from_typoscript, max_from_extension_config, timing_strategy and scoping_strategy.
SchedulerTimingStrategy
Info entry per processed transition, with the flushed cache tags and the active scoping strategy.
TemporalCacheSchedulerTask
Debug entries when the task starts and when it finds no transitions in the range it examined.

Errors are logged regardless of this setting, and so is the completion entry of the scheduler task, so a failing transition is visible without switching it on.

Log location

Log entries go to TYPO3's configured log writers; the default file writer writes to var/log/typo3_*.log.

grep TemporalCache var/log/typo3_*.log
Copied!

Example -------

advanced.debug_logging = 1
Copied!

Scheduler task 

The scheduler and hybrid timing strategies do not flush caches themselves. They rely on NetresearchTemporalCacheTaskTemporalCacheSchedulerTask, which finds the transitions that occurred since its last run and hands each one to the active timing strategy.

ext_localconf.php registers the task type in $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'], the list the Scheduler module builds its task selection from, so Scheduler → Create new task offers Temporal Cache: Process transitions.

Creating the task is not enough on its own either — the Scheduler needs a cron entry that runs it:

crontab
* * * * * /usr/bin/php /var/www/html/vendor/bin/typo3 scheduler:run
Copied!

scheduler:run executes every task that is due.

Verifying the setup 

vendor/bin/typo3 temporalcache:verify runs four checks:

  1. An index whose leading column is starttime, and one whose leading column is endtime, on both pages and tt_content
  2. scoping.strategy is one of global, per-page, per-content and timing.strategy is one of dynamic, scheduler, hybrid
  3. The time slot configuration, when harmonization is enabled
  4. The columns the queries rely on: starttime, endtime, hidden, deleted, sys_language_uid on both tables, plus pid on tt_content

It exits with 0 when every check passes and 1 otherwise. Add --verbose for the per-field table of the schema check.

Next steps 

Examples & presets 

Complete configurations, and what each of them actually changes.

For help choosing between them, see Optimization strategies and Decision guide.

Presets offered by the wizard 

The Wizard tab of the backend module offers these three presets. The values below are the ones it shows; the wizard does not write them — apply them in the Extension Manager or in config/system/additional.php.

Simple 

Global scoping, dynamic timing, no harmonization — the shipped defaults.

scoping.strategy = global
timing.strategy = dynamic
harmonization.enabled = 0
Copied!

Every temporal transition flushes every page cache, and the page cache lifetime is capped at the next transition anywhere on the site. Nothing beyond the extension itself has to be set up.

Balanced 

scoping.strategy = per-page
timing.strategy = hybrid
harmonization.enabled = 1
harmonization.slots = 00:00,06:00,12:00,18:00
Copied!

With timing.hybrid.pages and timing.hybrid.content left at their defaults, page transitions stay dynamic and content transitions are routed to the scheduler task. The cache lifetime follows the page rule, so it is still calculated on every page cache generation, narrowed to page transitions site-wide plus content transitions on the page being rendered.

Aggressive 

scoping.strategy = per-content
scoping.use_refindex = 1
timing.strategy = scheduler
harmonization.enabled = 1
harmonization.slots = 00:00,04:00,08:00,12:00,16:00,20:00
Copied!

The extension stops modifying the cache lifetime altogether. Invalidation happens only when the scheduler task processes a transition, and then it flushes exactly the pages on which the changed content appears, resolved through sys_refindex. This is the combination in which per-content scoping pays off — and the one that does nothing at all without the scheduler task.

Worked configurations 

Exact publication times matter 

Flash sales, embargoed articles: the transition must happen at the time the editor entered.

scoping.strategy = per-page
timing.strategy = dynamic
harmonization.enabled = 0
Copied!

Dynamic timing is the only strategy that expires the cache at the transition itself. Harmonization stays off so no timestamp is moved. Scoping is per-page because under dynamic timing the scoping strategy only supplies the next-transition timestamp, and per-page is the one that narrows that timestamp to the page being rendered; per-content would return the site-wide value here.

Many content transitions, menus must stay current 

scoping.strategy = per-content
scoping.use_refindex = 1
timing.strategy = hybrid
timing.hybrid.pages = dynamic
timing.hybrid.content = scheduler
Copied!

Page transitions keep their dynamic lifetime, which is what keeps menus correct. Content transitions are handed to the scheduler task, which flushes only the pages the content appears on. Note that the lifetime query still runs on every page cache generation: it follows the page rule, and that rule is dynamic here.

Fewer, grouped cache flushes 

harmonization.enabled = 1
harmonization.slots = 00:00,06:00,12:00,18:00
harmonization.tolerance = 3600
Copied!

Transitions within an hour of a slot are moved onto it, so several records share one transition time instead of each having its own. Records further than an hour from every slot keep their original times. Harmonization is applied when it is invoked — through the Harmonize selected action in the Content tab of the backend module, or through vendor/bin/typo3 temporalcache:harmonize — not when editors save a record.

Publication at one fixed time of day 

harmonization.enabled = 1
harmonization.slots = 09:00
harmonization.tolerance = 3600
Copied!

With a single slot, only timestamps within an hour of 09:00 are moved onto it. Raise harmonization.tolerance to cover the spread you actually want to absorb; a timestamp at 20:00 is 11 hours from the slot and needs a tolerance of at least 39600 to be moved.

Multi-language site 

No language-specific setting exists. Every transition query filters on the language of the current context, and the cache tags the scoping strategies emit are page-based, so language handling needs no configuration.

PHP configuration 

Complete configuration 

config/system/additional.php
<?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,
        'auto_round' => false,
    ],
    'advanced' => [
        'default_max_lifetime' => 86400,
        'debug_logging' => false,
    ],
];
Copied!

Every key is optional; the getters in NetresearchTemporalCacheConfigurationExtensionConfiguration supply the documented default for anything absent.

Environment-specific configuration 

config/system/additional.php
<?php

if (getenv('TYPO3_CONTEXT') === 'Development') {
    $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_temporal_cache']['advanced']['debug_logging'] = true;
}
Copied!

additional.php is read after the settings written by the Extension Manager, so assignments here override them.

Next steps 

Troubleshooting 

Diagnose and resolve common configuration issues.

Start here 

vendor/bin/typo3 temporalcache:verify
Copied!

The command checks the indexes and columns the queries need, the two strategy names, and the time slots when harmonization is enabled. It reports which check failed and exits with 1. See Verifying the setup for the full list of checks.

Cache not updating 

Symptoms

  • Temporal content does not appear or disappear at the scheduled time
  • Menus show pages whose starttime has not been reached

Checks

  1. Which timing strategy is active?

    vendor/bin/typo3 temporalcache:analyze
    Copied!

    Only dynamic expires the cache by itself. scheduler relies entirely on the scheduler task, and hybrid does for whichever content type is routed to it, so check that the task exists in the Scheduler module and that cron is running it. See Scheduler task.

  2. Do the indexes exist?

    The extension ships them in ext_tables.sql; TYPO3 creates them during the database compare, not at installation time.

    SHOW INDEX FROM pages WHERE Key_name LIKE 'idx_temporalcache%';
    SHOW INDEX FROM tt_content WHERE Key_name LIKE 'idx_temporalcache%';
    Copied!

    Expected on both tables: idx_temporalcache_starttime over (starttime, sys_language_uid) and idx_temporalcache_endtime over (endtime, sys_language_uid). If they are missing, run the database compare rather than creating them by hand:

    vendor/bin/typo3 extension:setup
    Copied!

    Admin Tools → Maintenance → Analyze Database Structure does the same from the backend.

  3. Is the record actually visible?

    The transition queries skip records that are deleted or hidden, and they filter on the language of the current context. A hidden record's starttime never triggers anything.

  4. Turn on debug logging.

    advanced.debug_logging = 1
    Copied!
    grep TemporalCache var/log/typo3_*.log | tail -50
    Copied!

    With dynamic timing, one entry per page cache generation shows the lifetime that was written and which maximum capped it.

High database load 

Symptoms

  • Slow page generation with timing.strategy = dynamic
  • Many MIN(starttime) / MIN(endtime) queries in the slow query log

What the extension queries

The dynamic strategy runs two MIN() queries per monitored table — one for starttime, one for endtime — on every page cache generation. With the two default tables that is four queries. Each table registered through TemporalMonitorRegistry adds two more.

The site-wide lookup used by global and per-content scoping is cached for the duration of the request; the two lookups of per-page scoping are not.

Options

  1. Make sure the indexes above exist — without them these are full table scans.
  2. Set timing.strategy = scheduler to remove the queries from page generation entirely. This needs the scheduler task; read Scheduler task first.
  3. Check the query plan:

    EXPLAIN SELECT MIN(starttime) FROM pages
    WHERE starttime > UNIX_TIMESTAMP()
      AND hidden = 0 AND deleted = 0
      AND sys_language_uid = 0;
    Copied!

    The plan should use one of the idx_temporalcache_* indexes instead of scanning the table.

Changing scoping.strategy does not reduce this load: under dynamic timing every scoping strategy performs the same kind of lookup, and per-content performs the site-wide one.

Harmonization not working 

Symptoms

  • The Content tab shows no harmonization column or no suggestions
  • Timestamps stay where they were

Checks

  1. harmonization.enabled = 1. While it is off, the service returns every timestamp unchanged and the backend module hides the column.
  2. The slots parse. Entries must be HH:MM or H:MM with hours 0-23 and minutes 0-59; anything else is dropped silently, and with no valid slot left nothing is harmonized. temporalcache:verify reports the parsed slots when harmonization is enabled.
  3. The tolerance is large enough. A timestamp is only moved when its nearest slot is at most harmonization.tolerance seconds away.

    Slots 00:00,12:00 with tolerance 3600:
    
    11:30 → nearest slot 12:00, 30 min away  → harmonized to 12:00
    10:30 → nearest slot 12:00, 90 min away  → left at 10:30
    Copied!

    harmonization.tolerance = 0 harmonizes nothing at all. The label in ext_conf_template.txt calls it "no limit", which is backwards.

  4. The distance is measured within the day. 23:30 is far from a 00:00 slot, not close to the next day's.
  5. Harmonization is invoked, not automatic. Nothing rewrites timestamps when an editor saves a record. Use Harmonize selected in Tools → Temporal Cache → Content, or:

    vendor/bin/typo3 temporalcache:harmonize
    Copied!

Scheduler task 

This version registers no scheduler task type, so the task cannot be created in System → Scheduler. Scheduler task describes what that means for the scheduler and hybrid timing strategies.

If a task type has been registered on your installation, verify that the Scheduler itself runs:

crontab -l | grep scheduler
vendor/bin/typo3 scheduler:run
Copied!

Configuration not taking effect 

Typos in strategy names are not errors. ScopingStrategyFactory and TimingStrategyFactory activate the strategy whose name matches the configured value and fall back to the highest-priority tagged strategy — global and dynamic — when none does. A misspelled per-contnet therefore behaves exactly like the default. temporalcache:verify flags both values as INVALID.

Out-of-range values are clamped, not rejected. advanced.default_max_lifetime of 0 or less is skipped in favour of 86400.

Two configuration sources. The Extension Manager writes to config/system/settings.php; config/system/additional.php is read afterwards, so an assignment there overrides the Extension Manager value. Check both files before concluding that a setting is ignored.

Getting help 

Collect before reporting:

vendor/bin/typo3 extension:list | grep temporal_cache
vendor/bin/typo3 temporalcache:verify
vendor/bin/typo3 temporalcache:analyze
grep TemporalCache var/log/typo3_*.log | tail -50
Copied!

Report at GitHub issues with the TYPO3 version, the extension version and the output above.

Next steps 

Backend module 

The extension registers a backend module Temporal Cache in the Tools section. It shows what temporal content exists, which transitions are coming up, which configuration is active, and it can apply harmonization to selected records.

Accessing the module 

Navigate to Tools > Temporal Cache.

Access
The module is registered for administrators only and is available in the Live workspace only. See Access and permissions.
TYPO3 versions
The extension supports TYPO3 12.4, 13 and 14 — see Installation.

The module menu offers three views. A fourth controller action, harmonize, is the endpoint the content view calls when applying harmonization; it has no view of its own.

Views 

📊 Dashboard 

Counters for temporal content, the active configuration, and a day-by-day timeline of the transitions due in the next seven days.

📝 Content 

The full list of temporal pages and content elements, with filters, harmonization suggestions and bulk harmonization.

⚙️ Configuration wizard 

A walkthrough of the scoping and timing strategies with three ready-made presets. The wizard shows settings, it does not write them.

💡 Tips and best practices 

Index checks, harmonization advice, troubleshooting and the permission model.

Doc header buttons 

Every view carries a reload button and a bookmark button. The dashboard additionally offers a View Content button that opens the content view.

There is no cache-flush, export or configuration-test button in the module. Flushing caches is done through the standard TYPO3 tools:

Flush all caches from the command line
vendor/bin/typo3 cache:flush
Copied!

Command-line equivalents 

Everything the module reads is also available on the command line, and harmonization can be previewed there before it is applied:

The commands behind the module views
vendor/bin/typo3 temporalcache:analyze
vendor/bin/typo3 temporalcache:list
vendor/bin/typo3 temporalcache:harmonize --dry-run
vendor/bin/typo3 temporalcache:verify
Copied!

See Command-line interface for the full reference.

Dashboard 

The dashboard is the entry view of the module. It shows how much temporal content exists, which configuration is active, and which transitions are due in the coming days.

All figures are read live when the view is opened; there is no stored history. The counters cover the live workspace across all languages, the timeline covers the default language.

Statistics cards 

Four cards across the top of the view.

Total Temporal Content
Number of pages and content elements that carry a start time or an end time, with the split into Pages and Content below the figure.
Active Content
Records that are visible at the moment the view is opened — the start time has passed or is unset, and the end time lies in the future or is unset.
Scheduled Content
Records whose start time lies in the future.
Transitions
Number of transition events in the next 30 days. A record with both a start time and an end time in that window contributes two events.

Current configuration 

Shows the active scoping strategy, the active timing strategy and whether harmonization is enabled. The values come from the extension configuration; change them under Admin Tools > Settings > Extension Configuration, see Configuration.

When harmonization is enabled and records exist whose start time would move, an additional hint appears with a View Harmonizable Content link that opens the content view with the harmonizable filter applied.

Upcoming transitions timeline 

Lists the transitions of the next seven days, grouped per day. Each day carries a badge with its number of transitions, and each entry shows:

  • the time of day
  • the title of the page or content element
  • a Start or End badge, colored green for start and red for end
  • the source record as table:uid

Days without transitions are omitted. When no transition falls into the next seven days, the card shows a note instead of the list.

Key performance indicators 

Average Transitions per Day
The number of days within the next 30 days that carry at least one transition.
Harmonization Potential
Only shown when harmonization is enabled. The number of records whose start time harmonization would move to a different slot.

Quick actions 

View All Temporal Content
Opens the content view without a filter.
Configuration Wizard
Opens the configuration wizard.
Harmonize Content
Only shown when harmonization is enabled and candidates exist. Opens the content view with the harmonizable filter applied.

The doc header additionally offers a reload button, a bookmark button, and a View Content button.

Next steps 

Content 

Lists every page and content element that carries a start time or an end time, and — when harmonization is enabled — lets you apply harmonization to selected records.

Filters 

Seven filter buttons above the table. The active filter is kept while paging.

All Content
Every record with a start time or an end time.
Pages Only
Records from the pages table.
Content Elements
Records from the tt_content table.
Active
Records that are visible right now.
Scheduled
Records whose start time lies in the future.
Expired
Records whose end time lies in the past.
Harmonizable
Records whose start time or end time harmonization would move to a different slot.

Table columns 

Type
Page or Content, depending on the source table.
UID
The record UID. UIDs are unique per table, not across tables.
Title
Page title or content element header, with a hidden badge when the record is hidden.
Start Time, End Time
Formatted as DD.MM.YYYY HH:MM, or - when the field is not set.
Status
Active when the record is visible now, Scheduled when its start time lies in the future, Expired otherwise.
Harmonization Suggestion
Only shown when harmonization is enabled. For the start time and the end time separately: the slot harmonization would move the value to, and the shift in minutes. Records without a suggestion show -.

Rows with a harmonization suggestion are highlighted. The list is not sortable and has no search field; use temporalcache:list on the command line for sorting, filtering and export.

Pagination 

The list shows 50 records per page. Page links appear below the table as soon as there is more than one page.

Harmonizing records 

The selection column, the suggestion column and the Harmonize Selected button appear only when harmonization is enabled in the extension configuration. A checkbox is rendered only for records that actually have a suggestion.

  1. Select one or more records, or use the checkbox in the table header to select all of them. Ctrl + A (Cmd + A on macOS) has the same effect while the focus is outside an input field.
  2. Harmonize Selected becomes active as soon as one record is selected.
  3. Confirm the dialog.
  4. The selected start and end times are written to the database, the page cache group is flushed, and the view reloads.

Next steps 

Configuration wizard 

Guided walkthrough of the available strategies, with three ready-made preset combinations.

Wizard steps 

The wizard is a single backend action that renders one step at a time, selected by a step parameter in the URL. Opening Configuration Wizard from the module menu starts at the welcome step.

Welcome 

Shows three figures for the current site:

  • the number of temporal records found
  • the number of days in the next 30 days that carry at least one transition
  • the number of records whose start time harmonization would move, if harmonization is enabled

Start Configuration continues to the analysis step.

Analysis 

Lists recommendations derived from the current configuration and the figures above. A recommendation is shown when one of these applies:

  • harmonization is disabled and more than 10 of the next 30 days carry transitions
  • the scoping strategy is global and more than 100 temporal content elements exist
  • the timing strategy is dynamic and more than 20 of the next 30 days carry transitions

When none applies, the step shows no recommendations. Continue to Presets leads to the presets step, Back returns to the welcome step.

Presets 

Shows the three preset combinations side by side, each with its scoping strategy, timing strategy and harmonization state. Apply Preset opens a confirmation dialog and then the notification described above — the preset is not written anywhere.

Custom Configuration leads to the custom step, Back returns to the analysis step.

Custom configuration 

A form with the three strategy choices, pre-selected from the current configuration:

Scoping strategy
global, per-page or per-content.
Timing strategy
dynamic, scheduler or hybrid.
Time slot harmonization
A single on/off switch. The slots and the tolerance are not part of this form; they are configured in the extension configuration.

Apply Configuration shows the notification and saves nothing. The form itself states that changes have to be applied in the extension configuration.

Summary 

A closing step with a link back to the dashboard. It is part of the module but no button in the wizard currently links to it.

Presets in detail 

The preset definitions live in the module controller. Enter the values shown here in the extension configuration to reproduce a preset.

Simple (Phase 1 compatible) 

Backward compatible with Phase 1: global scoping, dynamic timing, no harmonization.

Settings of the "Simple" preset
'scoping' => ['strategy' => 'global'],
'timing' => ['strategy' => 'dynamic'],
'harmonization' => ['enabled' => false],
Copied!

Balanced 

Per-page scoping with hybrid timing and harmonization on a six-hour grid.

Settings of the "Balanced" preset
'scoping' => ['strategy' => 'per-page'],
'timing' => ['strategy' => 'hybrid'],
'harmonization' => ['enabled' => true, 'slots' => '00:00,06:00,12:00,18:00'],
Copied!

Aggressive optimization 

Per-content scoping backed by the reference index, scheduler timing and harmonization on a four-hour grid.

Settings of the "Aggressive Optimization" preset
'scoping' => ['strategy' => 'per-content', 'use_refindex' => true],
'timing' => ['strategy' => 'scheduler'],
'harmonization' => ['enabled' => true, 'slots' => '00:00,04:00,08:00,12:00,16:00,20:00'],
Copied!

Next steps 

Tips and best practices 

Check the database indexes first 

The extension ships the indexes it needs in ext_tables.sql: an index led by starttime and one led by endtime, on pages and on tt_content. They are created by the schema migrator, not by the extension itself.

Create the indexes after installing or updating the extension
vendor/bin/typo3 extension:setup
Copied!
Confirm that they exist
vendor/bin/typo3 temporalcache:verify
Copied!

The same check appears in the Reports module. Without these indexes every temporal lookup falls back to a full table scan.

Using harmonization effectively 

Align the slots with your editorial rhythm
If articles are published at 09:00, 13:00 and 17:00, use exactly those times as slots. Timestamps are only moved when they lie within the configured tolerance of a slot, so slots far from your actual publishing times harmonize nothing.
Review the shifts before applying them
The Harmonization Suggestion column in the content view shows the shift in minutes per record. A large shift moves content visibility noticeably; decide per record whether that is acceptable.
Preview a bulk run on the command line
temporalcache:harmonize --dry-run lists every pending change without writing. Run it before harmonizing from the backend, since the backend writes bypass the DataHandler and leave no history entry to revert.
Start small
Harmonize one table at a time with --table=pages or --table=tt_content.

Managing temporal content 

Clean up expired records
The Expired filter lists everything past its end time.
Watch for clustering
The dashboard timeline shows how many transitions fall on the same day. Many transitions on one day mean many cache invalidations on that day; that is where harmonization pays off.
Export an inventory
The backend list has no export. Use temporalcache:list with --format=csv or --format=json.

Troubleshooting 

Cache is not updating 

  1. Run vendor/bin/typo3 temporalcache:verify and fix everything it reports.
  2. Check the timing strategy on the dashboard. With the scheduler or hybrid strategy, confirm that the scheduler task runs — see Scheduler task.
  3. Confirm that the affected record is listed at all, with vendor/bin/typo3 temporalcache:list --upcoming.

No harmonization suggestions appear 

  1. Harmonization must be enabled in the extension configuration — the suggestion column is not rendered otherwise.
  2. Check slots and tolerance with vendor/bin/typo3 temporalcache:verify, which validates the slot format and the tolerance range.
  3. A timestamp outside the tolerance of every slot is left alone by design; widen the tolerance or add slots.

Access and permissions 

The module is registered for administrators only ('access' => 'admin') and is available in the Live workspace only ('workspaces' => 'live'). Non-administrators do not see it in the Tools section.

To hide it from an administrator as well, use TSconfig:

User TSconfig or Group TSconfig
options.hideModules := addToList(tools_TemporalCache)
Copied!

Before applying harmonization the module additionally checks write access to every monitored table — by default pages and tt_content, plus any table registered through TemporalMonitorRegistry. Administrators pass this check unconditionally. When the check fails, the request is rejected with a message naming the tables the user cannot modify.

Next steps 

Command-line interface 

The extension registers four Symfony console commands. They are available through the TYPO3 console binary once the extension is installed and active.

List the commands of this extension
vendor/bin/typo3 list temporalcache
Copied!

Command overview 

Command Description Modifies data
temporalcache:analyze Analyze temporal content and provide cache statistics No
temporalcache:verify Verify database indexes and extension configuration No
temporalcache:list List all temporal content with transition information No
temporalcache:harmonize Harmonize temporal fields to configured time slots Yes

Only temporalcache:harmonize is registered as schedulable (schedulable: true in Configuration/Services.yaml). The other three commands are not offered in the scheduler.

Options shared with every Symfony command 

Besides the options documented per command, the standard Symfony console options apply. Two of them change the output of these commands:

-v, --verbose
Adds extra output sections. What each command adds is documented in its own section below.
-n, --no-interaction
Answers interactive questions with their default. temporalcache:harmonize is the only command that asks one, and its default answer is no — so a non-interactive run of that command writes nothing.

temporalcache:analyze 

Reads temporal content and reports statistics, upcoming transitions and — when harmonization is enabled — the reduction harmonization would achieve. The command never writes to the database and always exits with code 0.

Options 

Option Short Value Default Description
--workspace -w required 0 Workspace UID to analyze (0 = live workspace)
--language -l required 0 Language UID to analyze (-1 = all languages, 0 = default language)
--days -d required 30 Number of days to analyze for upcoming transitions

Output 

The command prints these sections in order:

Analysis context
Workspace, language, analysis period and the current server time.
Temporal content statistics
Total temporal items, split into pages and content elements, and the distribution across start time only, end time only and both. If no temporal content exists at all, the command stops here with a warning.
Upcoming transitions
The number of transitions found in the analysis period, followed by the five days carrying the most transitions. Each day is rated LOW (fewer than 5 transitions), MEDIUM (5 to 9) or HIGH (10 or more). With --verbose the next ten transitions are listed with time, type, table and title.
Harmonization impact analysis
Only when harmonization is enabled in the extension configuration. Compares the number of original transitions with the number remaining after harmonization and shows the resulting reduction. With --verbose the configured time slots and the tolerance are listed as well. When harmonization is disabled, the command prints a note instead of this section.
Extension configuration
Only with --verbose. Scoping strategy, timing strategy, harmonization state, slots, tolerance and the auto-round setting.

Examples 

Analyze the live workspace for the next 30 days
vendor/bin/typo3 temporalcache:analyze
Copied!
Analyze a workspace over a longer period, with the detailed sections
vendor/bin/typo3 temporalcache:analyze --workspace=1 --days=60 --verbose
Copied!
Analyze transitions across all languages
vendor/bin/typo3 temporalcache:analyze --language=-1
Copied!

temporalcache:verify 

Checks that the database and the extension configuration are in the state the extension needs. The command takes no options of its own and writes nothing.

Exit code 0 means every check passed, exit code 1 means at least one check failed. That makes the command usable as a health probe in monitoring or deployment pipelines.

Checks performed 

Database index verification
Requires an index led by starttime and an index led by endtime, on both pages and tt_content. An index over more columns satisfies the check as long as the temporal field is the leading column. The indexes ship with the extension in ext_tables.sql; apply them with vendor/bin/typo3 extension:setup.
Extension configuration verification
The scoping strategy must be global, per-page or per-content. The timing strategy must be dynamic, scheduler or hybrid. The harmonization state is reported but never fails the check.
Harmonization configuration verification
Runs only when harmonization is enabled. At least one time slot must be configured, every slot must match the H:MM or HH:MM pattern, and the tolerance must be greater than 0 and at most 86400 seconds. The auto-round setting is reported but never fails the check.
Database schema verification
pages must carry starttime, endtime, hidden, deleted and sys_language_uid; tt_content must carry those five plus pid. Without --verbose the command prints a single confirmation line; with --verbose it prints the full field-by-field table.

Examples 

Run all checks
vendor/bin/typo3 temporalcache:verify
Copied!
Run all checks and print the per-field schema table
vendor/bin/typo3 temporalcache:verify --verbose
Copied!
Use the exit code in a shell script
if vendor/bin/typo3 temporalcache:verify >/dev/null 2>&1; then
    echo "Temporal cache system healthy"
else
    echo "Temporal cache system reported issues"
fi
Copied!

temporalcache:list 

Lists pages and content elements that carry a start time or an end time. The command writes nothing. It exits with 1 when --table, --sort or --format receives a value outside its allowed set, and with 0 otherwise — including when the result set is empty.

Options 

Option Short Value Default Description
--table -t required none Filter by table; pages or tt_content
--workspace -w required 0 Workspace UID to list (0 = live workspace)
--language -l required 0 Language UID to list (-1 = all, 0 = default language)
--upcoming -u none off Show only content whose start time or end time lies in the future
--sort -s required uid Sort by uid, title, starttime, endtime or table
--format -f required table Output format; table, json or csv
--limit none required none Maximum number of records to output

Sorting by title or table is case-insensitive. Sorting by starttime or endtime places records without that field last. --limit is applied after filtering and sorting; a value of 0 or lower is ignored.

Output formats 

table
Human-readable output with a heading, a filter line and a table of table name, UID, title (truncated to 30 characters), start time, end time and the next transition. Warnings about an empty result set are printed in this format only.
json
A pretty-printed JSON array. Each object carries the keys table, uid, pid, title, starttime, starttime_formatted, endtime, endtime_formatted, language_uid, workspace_uid, hidden and deleted. The raw fields hold Unix timestamps or null; the _formatted fields hold Y-m-d H:i:s strings or null.
csv

Comma-separated output with the fixed header line:

Table,UID,PID,Title,StartTime,EndTime,Language,Workspace,Hidden,Deleted
Copied!

Titles are quoted and inner quotes are doubled. Timestamps are written as Y-m-d H:i:s, empty when the field is not set. Hidden and Deleted are written as 1 or 0.

In json and csv format an empty result set produces no output at all.

Examples 

List all temporal content of the live workspace
vendor/bin/typo3 temporalcache:list
Copied!
List the ten pages whose next start time comes first
vendor/bin/typo3 temporalcache:list --table=pages --upcoming --sort=starttime --limit=10
Copied!
Export the inventory for a spreadsheet
vendor/bin/typo3 temporalcache:list --format=csv > temporal-content.csv
Copied!
Export the inventory for further processing
vendor/bin/typo3 temporalcache:list --format=json > temporal-content.json
Copied!

temporalcache:harmonize 

Rounds starttime and endtime values to the configured time slots, so that fewer distinct transition timestamps remain and the cache is invalidated less often.

This is the only command that changes data.

Options 

Option Short Value Default Description
--dry-run none none off Preview changes without modifying the database
--workspace -w required 0 Workspace UID to harmonize (0 = live workspace)
--language -l required 0 Language UID to harmonize (0 = default language)
--table -t required none Limit to a single table; pages or tt_content

What the command does 

  1. Aborts with exit code 1 when harmonization is disabled in the extension configuration, or when --table receives a value other than pages or tt_content.
  2. Prints the harmonization context: mode, workspace, language, table filter, configured time slots and tolerance.
  3. Loads the temporal content of the selected workspace and language and applies the table filter.
  4. Calculates the harmonized timestamp for every start time and end time and collects the records where the value would change. With --verbose the first ten pending changes are listed with their time shift.
  5. In live mode, asks Proceed with harmonization? — the default answer is no. No option confirms it up front; running with --no-interaction takes the default and writes nothing.
  6. Applies the changes, reports how many records were updated and how many failed, and flushes the pages cache group.
  7. Prints the impact analysis: number of changes, unique timestamps before and after, and the resulting reduction.

The command exits with 0 in every case after the configuration check has passed — including when nothing needs harmonizing and when the confirmation is declined. Individual failed record updates are counted and reported, but do not change the exit code.

Examples 

Preview the changes without touching the database
vendor/bin/typo3 temporalcache:harmonize --dry-run
Copied!
Preview the changes including the first ten records
vendor/bin/typo3 temporalcache:harmonize --dry-run --verbose
Copied!
Harmonize pages only, after reviewing the dry run
vendor/bin/typo3 temporalcache:harmonize --table=pages
Copied!

Next steps 

  • Configuration — extension configuration reference, including the harmonization slots and tolerance that temporalcache:harmonize reads
  • Backend module — the same data in the TYPO3 backend
  • TYPO3 Reports module — status reporting inside the TYPO3 Reports module

TYPO3 Reports module 

System status report 

The Temporal Cache extension contributes five status entries to TYPO3's built-in Reports module.

Accessing the report 

  1. Log in to the TYPO3 backend as an administrator
  2. Navigate to: Admin Tools > Reports > Status Report
  3. Scroll to the Temporal Cache section

The report displays in the standard TYPO3 Reports interface with color-coded status indicators:

  • Green (OK): Everything working properly, no action required
  • Blue (INFO): Informational, no action required
  • Yellow (WARNING): Non-critical issues or optimization recommendations
  • Red (ERROR): Critical issues requiring immediate attention

Report sections 

Extension Configuration 

What it shows:

  • Current scoping strategy (global, per-page, per-content)
  • Current timing strategy (dynamic, scheduler, hybrid)
  • Harmonization status (enabled/disabled)
  • Reference index usage
  • Recommendations when global scoping or dynamic timing is active

Status levels:

  • OK: Both strategy values are valid; any recommendations appear in the message text
  • ERROR: The scoping or timing strategy holds a value the extension does not know

Actions:

If you see an error or warning:

  1. Navigate to: Admin Tools > Settings > Extension Configuration
  2. Locate temporal_cache in the list
  3. Review and adjust the settings based on recommendations
  4. Save changes

Database Indexes 

What it shows:

  • Whether an index led by starttime and an index led by endtime exist on pages and tt_content
  • The names of the missing indexes, if any

Status levels:

  • OK: All required indexes are present
  • ERROR: Missing indexes detected, or the index list could not be read

Actions:

If indexes are missing:

  1. Navigate to: Admin Tools > Maintenance > Analyze Database Structure
  2. Review the proposed changes
  3. Apply the schema updates to create missing indexes
  4. Return to the Reports module to verify indexes are now present

The indexes ship with the extension in ext_tables.sql; the schema update creates them.

Performance impact:

Without them, every temporal content lookup is a full table scan instead of an index lookup.

Temporal Content Statistics 

What it shows:

  • Total number of pages and content elements with temporal fields
  • Distribution of starttime, endtime, and combined usage
  • Next upcoming transition time and the time remaining until it

Status levels:

  • OK: Temporal content found and being managed
  • WARNING: No temporal content found (extension is active but unused)
  • ERROR: Failed to retrieve statistics

Understanding the data:

  • Total Items: All pages and content elements with starttime or endtime set
  • Pages: Number of temporal pages (affects menu visibility)
  • Content Elements: Number of temporal content elements
  • With Start Date Only: Content that becomes visible at a specific time
  • With End Date Only: Content that becomes hidden at a specific time
  • With Both Dates: Content visible within a specific time window

Harmonization 

What it shows:

When harmonization is disabled:

  • Information about harmonization benefits
  • Recommendation to enable it if you have more than 10 transitions per day

When harmonization is enabled:

  • Current time slot configuration
  • Tolerance setting
  • Auto-round flag, which the report shows for reference only — nothing rounds timestamps on save
  • The reduction harmonization would achieve on the current content, as a percentage

Status levels:

  • INFO: Harmonization is disabled
  • OK: Harmonization is enabled

The measured reduction changes the message, not the severity: above 30 percent it is called significant, above 10 percent moderate, and below that the message suggests adjusting slots or tolerance.

Understanding cache reduction:

Harmonization reduces cache churn by rounding transition times to predefined slots. For example:

Without harmonization (3 separate cache invalidations):

  • Content A: 00:05
  • Content B: 00:15
  • Content C: 00:45

With harmonization to 00:00 slot (1 cache invalidation):

  • Content A: 00:00
  • Content B: 00:00
  • Content C: 00:00

When to enable:

  • You have more than 10 transitions per day — the threshold the report itself recommends at
  • You want to reduce cache invalidation frequency

When to disable:

  • You have few temporal items and few transitions
  • Exact timing of content visibility is critical
  • The reduction reported for your content is low; the report calls it low below 10 percent

Upcoming Transitions 

What it shows:

  • Total transitions scheduled in the next 7 days
  • Number of days carrying transitions, and a breakdown of the first five of them
  • A high-volume note when the 7-day average exceeds 20 transitions per day

Status levels:

  • OK: Normal transition volume, and also when no transition is scheduled at all
  • WARNING: More than 20 transitions per day on average across the next 7 days
  • ERROR: The transitions could not be read

Understanding transition impact:

Each transition can trigger cache invalidation depending on your scoping strategy:

  • Global scoping: All page caches invalidated on every transition
  • Per-page scoping: Only affected pages invalidated
  • Per-content scoping: Only pages containing affected content invalidated

High volume recommendations:

If you see a high transition volume warning (>20 per day):

  1. Consider enabling harmonization to group transitions
  2. Evaluate if scheduler-based timing would be more efficient
  3. Review if per-content scoping can reduce invalidation scope

Common scenarios 

Scenario 1: Extension Just Installed 

Expected status:

  • Extension Configuration: OK (default settings)
  • Database Indexes: ERROR (before the database schema update is applied)
  • Temporal Content: WARNING (no content found)
  • Harmonization: INFO (disabled by default)
  • Upcoming Transitions: OK (none scheduled)

Actions:

  1. Run database schema update to create indexes
  2. Start adding temporal content (pages/content with starttime/endtime)
  3. Return to Reports module to verify system status

Scenario 2: Production Site with Temporal Content 

Expected status:

  • Extension Configuration: OK
  • Database Indexes: OK
  • Temporal Content: OK (showing statistics)
  • Harmonization: OK or INFO (depending on configuration)
  • Upcoming Transitions: OK (showing schedule)

Actions:

  • Monitor the report periodically (weekly recommended)
  • Review harmonization recommendations if transition volume is high
  • Check for configuration optimization suggestions

Scenario 3: Performance Issues Detected 

Symptoms in report:

  • Database Indexes: ERROR (missing indexes)
  • Upcoming Transitions: WARNING (high volume)
  • Harmonization: INFO (disabled)

Resolution steps:

  1. Immediate: Create missing database indexes
  2. Short-term: Enable harmonization to reduce cache churn
  3. Long-term: Consider per-content scoping if using global scoping

Scenario 4: No Temporal Content Found 

Status:

  • Temporal Content: WARNING
  • Upcoming Transitions: OK

Possible causes:

  1. No pages or content elements have starttime/endtime set
  2. Content exists but is in a different workspace
  3. Content is in a different language

Actions:

  1. Verify temporal content exists in the backend
  2. Check if you're viewing the correct workspace (report shows live workspace by default)
  3. If no temporal content exists, consider if the extension is needed

Automation and monitoring 

CLI command alternative 

For automation and monitoring systems, use the CLI verify command:

Verification on the command line
# Quick verification (exit code 0 = OK, 1 = issues)
vendor/bin/typo3 temporalcache:verify

# Verbose output for logs
vendor/bin/typo3 temporalcache:verify --verbose
Copied!

The two overlap but are not identical. Both check the database indexes and the strategy values. Only temporalcache:verify validates the harmonization slot format, the tolerance range and the presence of the required table columns. Only the Reports module shows content statistics, the next transition and the upcoming transition volume. See temporalcache:verify for the full list of checks.

Integration with monitoring systems 

The verify command can be integrated with monitoring tools:

Nagios/Icinga:

#!/bin/bash
# /usr/local/nagios/libexec/check_typo3_temporal_cache.sh

cd /var/www/html/typo3
vendor/bin/typo3 temporalcache:verify >/dev/null 2>&1

if [ $? -eq 0 ]; then
    echo "OK - Temporal Cache system healthy"
    exit 0
else
    echo "CRITICAL - Temporal Cache system issues detected"
    exit 2
fi
Copied!

Cron-based monitoring:

# Check daily and email on failure
0 8 * * * cd /var/www/html/typo3 && vendor/bin/typo3 temporalcache:verify || mail -s "TYPO3 Temporal Cache Issues" admin@example.com < /dev/null
Copied!

Troubleshooting 

Report Not Visible 

Symptom: Temporal Cache section does not appear in Reports module

Causes:

  1. Extension not installed or activated
  2. Cache not cleared after installation
  3. Missing service registration

Solutions:

  1. Verify extension is installed: Admin Tools > Extensions
  2. Clear all caches: Admin Tools > Maintenance > Flush Cache
  3. Check if Services.yaml is properly loaded (check system log)

Database Index Check Fails 

Symptom: Cannot verify indexes, error message displayed

Causes:

  1. Database connection issues
  2. Insufficient database permissions
  3. Missing database tables

Solutions:

  1. Check database connection in Install Tool
  2. Verify database user has SELECT privileges on schema tables
  3. Run database schema update to ensure tables exist

Statistics Show Zero Items 

Symptom: Report shows 0 temporal items but content exists

Causes:

  1. Content is in a workspace (report shows live workspace)
  2. Content has been deleted but not purged
  3. starttime/endtime fields are set to 0 (not set)

Solutions:

  1. Use temporalcache:list to verify content: vendor/bin/typo3 temporalcache:list
  2. Check workspace settings in the backend
  3. Verify starttime/endtime fields have actual timestamps (not 0)

Best practices 

Regular Monitoring 

  • Weekly: Review the Reports module status
  • Monthly: Analyze transition patterns and harmonization impact
  • Quarterly: Review configuration and optimize based on usage patterns

Before Major Events 

Before high-traffic periods or major content updates:

  1. Verify all database indexes are present
  2. Review upcoming transitions schedule
  3. Confirm harmonization settings are optimal
  4. Test cache invalidation is working correctly

After Configuration Changes 

After modifying extension settings:

  1. Check Reports module to verify configuration is valid
  2. Review recommendations for new configuration
  3. Test with sample temporal content
  4. Monitor frontend performance

Database Maintenance 

After database updates or migrations:

  1. Verify database indexes still exist
  2. Run verify command to ensure schema is complete
  3. Check for any database-related errors in Reports module

Performance optimization 

Based on Reports Module Data 

High transition volume — the report warns above 20 per day:

  • Enable harmonization
  • Consider scheduler-based timing strategy
  • Use per-content scoping instead of global

Low transition volume:

  • Harmonization may not provide significant benefit
  • Dynamic timing strategy is efficient
  • Global scoping is acceptable for small sites

Many temporal items — the report suggests moving away from dynamic timing above 100:

  • Use per-content scoping for minimal cache invalidation
  • Enable reference index usage
  • Consider scheduler-based timing to avoid per-request calculations

Mixed workload:

  • Use hybrid timing strategy (dynamic for pages, scheduler for content)
  • Enable harmonization with appropriate time slots
  • Monitor cache reduction percentage

Architecture 

The gap this extension fills 

TYPO3's page cache is invalidated by two mechanisms.

Event-driven invalidation
A cache entry is dropped when the data behind it changes, for example when an editor saves a page.
Tag-based invalidation
flushByTag() drops every entry carrying a given tag.

Neither reacts to the passage of time. A page or content element with starttime or endtime changes its visibility at a fixed moment without anybody editing a record, so no invalidation is triggered. The cache entry keeps the visibility snapshot taken at render time until its relative lifetime runs out.

Render at 09:00           Cache entry written with a relative lifetime
├─ element A: hidden      (starttime 10:00 has not been reached)
└─ element B: visible

10:00                     Nothing edits a record, so nothing invalidates
                          the entry. Element A stays hidden until the
                          lifetime expires.
Copied!

The extension closes that gap by shortening the page cache lifetime so it ends at the next temporal transition instead of at an arbitrary later moment.

Entry point: the cache lifetime event 

TYPO3 dispatches TYPO3\CMS\Frontend\Event\ModifyCacheLifetimeForPageEvent while a page cache entry is being written. Netresearch\TemporalCache\EventListener\TemporalCacheLifetime is registered for exactly that event class in Configuration/Services.yaml, and it is the extension's only frontend hook.

Classes/EventListener/TemporalCacheLifetime.php (error handling and debug logging omitted)
final class TemporalCacheLifetime
{
    public function __construct(
        private readonly ExtensionConfiguration $extensionConfiguration,
        private readonly ScopingStrategyInterface $scopingStrategy,
        private readonly TimingStrategyInterface $timingStrategy,
        private readonly Context $context,
        private readonly LoggerInterface $logger
    ) {
    }

    public function __invoke(ModifyCacheLifetimeForPageEvent $event): void
    {
        $lifetime = $this->timingStrategy->getCacheLifetime($this->context, $event->getPageId());

        if ($lifetime !== null) {
            $maxLifetime = $this->determineMaxLifetime($event->getRenderingInstructions());
            $event->setCacheLifetime(\min($lifetime, $maxLifetime));
        }
    }
}
Copied!

The listener runs no queries of its own. It asks the active timing strategy for a lifetime and caps the answer. A null lifetime — what the scheduler timing strategy always returns — leaves TYPO3's own lifetime untouched.

The whole __invoke() body is wrapped in a try/catch (Throwable). A failing strategy is logged as an error and the page renders with TYPO3's lifetime; it never breaks page rendering.

Cap on the calculated lifetime 

determineMaxLifetime() resolves the upper bound in this order:

  1. cache_period from the rendering instructions, if it is set and greater than zero (TypoScript config.cache_period)
  2. advanced.default_max_lifetime from the extension configuration, if greater than zero
  3. 86400

The listener also caps the value the timing strategy already capped, because DynamicTimingStrategy limits its own result to advanced.default_max_lifetime independently.

Registration 

Configuration/Services.yaml (arguments omitted)
services:
  Netresearch\TemporalCache\EventListener\TemporalCacheLifetime:
    public: true
    tags:
      - name: event.listener
        identifier: 'temporal-cache/modify-cache-lifetime'
        event: TYPO3\CMS\Frontend\Event\ModifyCacheLifetimeForPageEvent
        method: '__invoke'
Copied!

Strategy selection and wiring 

Two independent strategy families decide what happens:

Scoping strategy (ScopingStrategyInterface)
Answers which records a transition lookup covers and which cache tags a transition flushes. Implementations: GlobalScopingStrategy, PerPageScopingStrategy, PerContentScopingStrategy.
Timing strategy (TimingStrategyInterface)
Answers when the invalidation happens — through a shortened cache lifetime, through a background scheduler run, or a mix of the two. Implementations: DynamicTimingStrategy, SchedulerTimingStrategy, HybridTimingStrategy.

Both interfaces extend Netresearch\TemporalCache\Service\NamedStrategyInterface, which declares the single method getName(): string.

How a strategy is activated 

Every strategy is registered as a service carrying one of two tags:

  • nr_temporal_cache.scoping_strategy
  • nr_temporal_cache.timing_strategy

ScopingStrategyFactory and TimingStrategyFactory receive all services carrying their family's tag through Symfony's !tagged_iterator:

Configuration/Services.yaml
Netresearch\TemporalCache\Service\Scoping\ScopingStrategyFactory:
  public: true
  arguments:
    $strategies: !tagged_iterator 'nr_temporal_cache.scoping_strategy'
    $extensionConfiguration: '@Netresearch\TemporalCache\Configuration\ExtensionConfiguration'

Netresearch\TemporalCache\Service\Scoping\ScopingStrategyInterface:
  alias: Netresearch\TemporalCache\Service\Scoping\ScopingStrategyFactory
  public: false
Copied!

Both factories share the selection logic in the trait Netresearch\TemporalCache\Service\SelectsNamedStrategy:

Classes/Service/SelectsNamedStrategy.php
private function selectNamedStrategy(
    iterable $strategies,
    string $configuredName,
    string $emptyMessage
): NamedStrategyInterface {
    $firstStrategy = null;

    foreach ($strategies as $strategy) {
        $firstStrategy ??= $strategy;

        if ($strategy->getName() === $configuredName) {
            return $strategy;
        }
    }

    return $firstStrategy ?? throw new RuntimeException($emptyMessage);
}
Copied!

The configured name is scoping.strategy respectively timing.strategy from the extension configuration. Three consequences follow from this code:

  • The tags carry no identifier attribute. A strategy is identified by its getName() return value alone.
  • When no name matches, the first tagged service wins. GlobalScopingStrategy and DynamicTimingStrategy carry priority: 100 in Configuration/Services.yaml so that they come first and hold that fallback.
  • When a family has no tagged service at all, the factory throws a RuntimeException.

Each factory implements its own family interface and delegates every call to the selected strategy, and the interface name is aliased to the factory. Anything type-hinting ScopingStrategyInterface or TimingStrategyInterface therefore receives the factory and, through it, the configured strategy.

Adding a strategy from another extension 

Because the factories iterate a tag rather than a hard-coded list, a strategy declared in another extension needs nothing but the tag:

EXT:my_extension/Classes/Scoping/RootlineScopingStrategy.php
namespace MyVendor\MyExtension\Scoping;

use Netresearch\TemporalCache\Domain\Model\TemporalContent;
use Netresearch\TemporalCache\Service\Scoping\ScopingStrategyInterface;
use TYPO3\CMS\Core\Context\Context;

final class RootlineScopingStrategy implements ScopingStrategyInterface
{
    public function getCacheTagsToFlush(TemporalContent $content, Context $context): array
    {
        return ['pageId_' . $content->pid];
    }

    public function getNextTransition(Context $context, ?int $pageId = null): ?int
    {
        return null;
    }

    public function getName(): string
    {
        return 'rootline';
    }
}
Copied!
EXT:my_extension/Configuration/Services.yaml
services:
  MyVendor\MyExtension\Scoping\RootlineScopingStrategy:
    tags:
      - { name: 'nr_temporal_cache.scoping_strategy' }
Copied!

Setting scoping.strategy to rootline then activates it.

Transition lookups 

All temporal queries live in Netresearch\TemporalCache\Domain\Repository\TemporalContentRepository (contract: TemporalContentRepositoryInterface). Three methods feed the strategies:

Classes/Domain/Repository/TemporalContentRepositoryInterface.php
// Earliest transition across every monitored table (site-wide)
public function getNextTransition(
    int $currentTimestamp,
    int $workspaceUid = 0,
    int $languageUid = 0
): ?int;

// Earliest transition in the pages table only
public function getNextPageTransition(
    int $currentTimestamp,
    int $workspaceUid = 0,
    int $languageUid = 0
): ?int;

// Earliest transition in the content tables, restricted to one pid
public function getNextContentTransitionForPage(
    int $pageId,
    int $currentTimestamp,
    int $workspaceUid = 0,
    int $languageUid = 0
): ?int;
Copied!

Each of them runs one MIN() query per monitored table and per temporal field (starttime, endtime) and returns the smallest non-null result. With the two default tables that is four queries.

findMinTransitionForTable() builds every query the same way:

  • removeAll() on the restrictions. TYPO3's StartTimeRestriction/EndTimeRestriction would hide exactly the future records the lookup needs.
  • MIN(<field>) selected as a literal, WHERE <field> > :now. Records with 0 are excluded by that comparison, so no separate != 0 clause exists.
  • The table's deleted and disabled columns, resolved from TCA, each compared to 0. When no TCA is available the query simply runs without them.
  • pid = :pageId when the caller passed a page id.
  • The workspace clause: for workspace 0 it matches `t3ver_wsid = 0 OR t3ver_wsid IS NULL``, otherwise ``t3ver_wsid = :workspace`.
  • sys_language_uid = :language whenever the language id is >= 0.

Request-level memoization 

getNextTransition() — the site-wide lookup only — is memoized in Netresearch\TemporalCache\Service\Cache\TransitionCache, a singleton keyed by timestamp, workspace id and language id. Repeated site-wide lookups within the same request and the same second are answered from memory. getNextPageTransition() and getNextContentTransitionForPage() are not memoized.

Indexes 

ext_tables.sql adds two composite indexes per default table so the MIN() aggregation and the > :now range scan can be served from an index:

  • pages: idx_temporalcache_starttime (starttime, sys_language_uid) and idx_temporalcache_endtime (endtime, sys_language_uid)
  • tt_content: the same two indexes

temporalcache:verify checks that these indexes exist. Tables registered by other extensions get no index from this extension.

What each scoping strategy does 

A scoping strategy answers two separate questions, and the answers do not have to agree.

getNextTransition()
Used by DynamicTimingStrategy to compute a cache lifetime.
getCacheTagsToFlush()
Used by SchedulerTimingStrategy to flush caches from the scheduler task. DynamicTimingStrategy never calls it.
Strategy getNextTransition() covers getCacheTagsToFlush() returns
global Every monitored table, site-wide. The page id is ignored. ['pages'] — the tag every page cache entry carries.
per-page The pages table site-wide, plus the content tables restricted to the rendered page. Falls back to the site-wide lookup when no page id is available. ['pageId_<uid>'] for a page, ['pageId_<pid>'] for a content element.
per-content Every monitored table, site-wide — the same lookup as global. One pageId_<uid> tag per page that sys_refindex reports for the element.

Refindex resolution 

PerContentScopingStrategy resolves the affected pages through Netresearch\TemporalCache\Service\RefindexService, which reads sys_refindex. It falls back to the element's own pid when scoping.use_refindex is off, when the lookup returns no page, or when it throws. Pages are never resolved through the refindex — a page transition always yields the page's own tag.

What each timing strategy does 

Strategy getCacheLifetime() processTransition()
dynamic Seconds until the scoping strategy's next transition, capped at advanced.default_max_lifetime. 60 if the transition is already in the past, advanced.default_max_lifetime if there is none. No-op. Expiry alone does the work.
scheduler null — the listener leaves TYPO3's lifetime alone. Flushes every tag the scoping strategy returns from the pages cache.
hybrid Delegates to the strategy configured under timing.hybrid.pages. Delegates per record: timing.hybrid.pages for a page, timing.hybrid.content for anything else.

The scheduler task 

Netresearch\TemporalCache\Task\TemporalCacheSchedulerTask is what drives processTransition(). Each run reads the last-run timestamp from TYPO3's Registry (namespace tx_temporalcache, key scheduler_last_run), asks findTransitionsInRange() for every transition since then, hands each one to the active timing strategy, and writes the new timestamp back. A transition that fails is logged and the run continues with the next one.

The interval is whatever frequency the task is given in the Scheduler module.

Workspace and language awareness 

All three scoping strategies read the current context through the trait Netresearch\TemporalCache\Service\Scoping\ResolvesContextAspects:

Classes/Service/Scoping/ResolvesContextAspects.php
$workspaceId = $context->getPropertyFromAspect('workspace', 'id', 0);
$languageId = $context->getPropertyFromAspect('language', 'id', 0);
Copied!

Both values are passed down into every query, so a workspace preview and each language resolve their own next transition and therefore their own cache lifetime. One frontend request carries one workspace and one language, so the number of queries does not grow with the number of languages configured on the site.

Extensibility 

Monitoring additional tables 

Additional tables are registered with Netresearch\TemporalCache\Service\TemporalMonitorRegistry. The registry is an autowired singleton and registerTable() is an instance method, so obtain it through constructor injection — there is no static registration API:

EXT:my_extension/Classes/Service/NewsTemporalRegistration.php
namespace MyVendor\MyExtension\Service;

use Netresearch\TemporalCache\Service\TemporalMonitorRegistry;

final class NewsTemporalRegistration
{
    public function __construct(
        private readonly TemporalMonitorRegistry $monitorRegistry
    ) {
        $this->monitorRegistry->registerTable(
            'tx_news_domain_model_news',
            ['uid', 'pid', 'title', 'starttime', 'endtime', 'hidden', 'deleted', 'sys_language_uid']
        );
    }
}
Copied!

The second argument is optional. Omitting it applies the default field list, which is the one shown above.

TemporalContentRepository queries every table returned by TemporalMonitorRegistry::getAllTables(), so each registered table adds two MIN() queries per lookup. getNextContentTransitionForPage() queries every registered table except pages, which requires those tables to carry a pid.

Custom cache lifetime logic 

EXT:my_extension/Classes/EventListener/CustomTemporalLogic.php
namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Frontend\Event\ModifyCacheLifetimeForPageEvent;

final class CustomTemporalLogic
{
    public function __invoke(ModifyCacheLifetimeForPageEvent $event): void
    {
        $customTransition = $this->getCustomTransition($event->getPageId());

        if ($customTransition !== null) {
            $event->setCacheLifetime(
                \min($event->getCacheLifetime(), \max(0, $customTransition - \time()))
            );
        }
    }

    private function getCustomTransition(int $pageId): ?int
    {
        return null;
    }
}
Copied!
EXT:my_extension/Configuration/Services.yaml
services:
  MyVendor\MyExtension\EventListener\CustomTemporalLogic:
    tags:
      - name: event.listener
        identifier: 'my-extension/custom-temporal-logic'
        event: TYPO3\CMS\Frontend\Event\ModifyCacheLifetimeForPageEvent
        after: 'temporal-cache/modify-cache-lifetime'
Copied!

Known limitations 

Only the page cache is addressed
The listener sets the page cache lifetime. Other caches keep whatever lifetime their own code assigns.
Per-second granularity
Transitions are Unix timestamps; nothing finer is possible.
No cross-page dependency detection with dynamic timing
per-page scoping does not shorten a page's lifetime for content embedded from elsewhere, and per-content scoping only narrows flush tags.
Scheduler timing flushes only what the scoping strategy names
With per-page or per-content scoping a page transition flushes only that page's own tag, so menus on other pages are not refreshed by the scheduler run. global scoping flushes the pages tag and does refresh them.
Additional tables get no indexes
ext_tables.sql covers pages and tt_content. A registered table needs its own index on starttime and endtime.

Next steps 

Approach, limits and a core solution 

Overview 

This chapter explains why the extension works the way it does, what the approach cannot do, and what a solution inside TYPO3 core would have to provide to make the extension unnecessary.

Nothing here describes committed work in TYPO3 core. There is no accepted RFC, no target version and no timeline for a core solution; treat the section on it as a description of the missing capability, not as a roadmap.

What the extension does today 

TYPO3's cache API accepts a relative lifetime — "keep this for N seconds". It has no absolute expiration — "keep this until timestamp T". Temporal visibility is an absolute-expiration problem, so the extension approximates one in two ways.

Shorten the lifetime (timing.strategy = dynamic)
The listener on ModifyCacheLifetimeForPageEvent computes nextTransition - time() and sets that as the entry's lifetime.
Classes/EventListener/TemporalCacheLifetime.php (condensed)
public function __invoke(ModifyCacheLifetimeForPageEvent $event): void
{
    $lifetime = $this->timingStrategy->getCacheLifetime($this->context, $event->getPageId());

    if ($lifetime !== null) {
        $maxLifetime = $this->determineMaxLifetime($event->getRenderingInstructions());
        $event->setCacheLifetime(\min($lifetime, $maxLifetime));
    }
}
Copied!
Flush tags from a scheduled task (timing.strategy = scheduler)
The listener leaves the lifetime alone. TemporalCacheSchedulerTask asks for every transition since its last run and flushes the cache tags the scoping strategy names for each of them.

hybrid combines the two, choosing per record type. See What each timing strategy does for the exact behavior of each strategy.

Properties of this approach 

Works with the released TYPO3 versions
^12.4 || ^13.0 || ^14.0, no core patch required.
Nothing to configure for it to work
The defaults (global scoping, dynamic timing) are active on installation.
Runs queries on every page cache write
With dynamic timing, each write costs two MIN() queries per monitored table — four with the default pages/tt_content pair. scheduler timing moves that cost out of page generation entirely.
Cannot express per-entry absolute expiration
The only lever is the relative lifetime of the entry currently being written, so an upcoming transition anywhere in the scope shortens the lifetime of the entry, whether or not that entry actually shows the affected record. This is why global scoping expires all page caches at every transition.
Cannot follow arbitrary dependencies
With dynamic timing, per-page scoping does not see content embedded from another page through CONTENT/RECORDS, and per-content scoping narrows only the flush tags, not the lifetime.

What a core solution would need 

Two capabilities are missing from TYPO3, and both would have to come from core.

Absolute expiration in the cache API 

A cache entry would have to be able to carry an absolute expiry timestamp alongside its relative lifetime, so the frontend could say "this entry is valid until 2026-10-28 14:30" without a background job and without shortening anything else.

With that, the extension's work would reduce to attaching the transition timestamp of the records that were actually rendered — per entry, with no site-wide lookup and no scheduler.

Temporal dependency tracking 

The deeper gap is that nothing records which temporal records contributed to a cache entry. If the rendering pipeline tracked that — the way cache tags already track record identity — the expiry timestamp could be derived automatically and correctly, including for content embedded across pages, which is exactly the case this extension cannot cover.

That is a change to the rendering pipeline, not to the cache API alone.

If core gains these capabilities 

Nothing in this extension currently detects core capabilities or switches APIs automatically; that would have to be built when there is an API to build against.

What would happen in practice:

  1. The extension would gain support for the new API on the TYPO3 versions that have it, keeping the current implementation for the versions that do not.
  2. Once every supported TYPO3 version carried the core solution, the extension would have nothing left to do and could be removed from a project.

Until then, the settings described in Configuration are the only lever.

When this approach fits 

It fits when
Temporal content exists and stale menus or stale content elements are a real editorial problem; and the cache behavior of the chosen configuration has been measured on the site in question.
It does not fit when
No record uses starttime/endtime — the extension then only adds queries; or the site cannot absorb the cache churn of the configuration it needs and no scoping/timing combination brings it down far enough.

Both cases are judgment calls about a specific site. Decision guide walks through which configuration matches which shape of site, and Alternative approaches covers approaches that do not involve this extension.

Feedback 

The interesting feedback for this problem is the concrete behavior of a real site: which scoping and timing combination was chosen, and what happened to the cache hit ratio.

Next steps 

📘 How it is implemented 

The listener, the two strategy families, the queries they run and how they are wired together.

⚡ What the defaults cost 

What each scoping and timing combination does to the cache, and how to narrow the default.

🔧 Install the extension 

Requirements, installation and verification.

🎯 Follow the core issue 

The Forge issue this extension addresses.