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.
Important
Extension Status: Beta (version 0.9.0, state beta in
ext_emconf.php). v0.9.0 is the first stable release. Beta
means the API may still change before 1.0 — test before using in
production.
Approach: TYPO3's cache API has no absolute expiration, so the extension
approximates one by shortening relative lifetimes or by flushing tags from a
scheduled task. A solution inside TYPO3 core would need neither.
See Approach, limits and a core solution for what the approach cannot do, and what core would have to
provide to make the extension unnecessary.
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
Warning
Out of the box the extension uses global scoping with dynamic timing
(scoping.strategy = global, timing.strategy = dynamic).
In that combination the lifetime of every page cache entry is cut back to the
earliest upcoming transition anywhere on the site, so all page caches expire at every
temporal transition, not only the caches of the affected pages.
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.
ext_emconf.php declares version 0.9.0 and state beta.
v0.9.0 is the first stable release.
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.
Architecture — how the listener, strategies and queries fit together
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.
Important
The default configuration — scoping.strategy = global and
timing.strategy = dynamic — shortens the lifetime of every page cache entry to
the earliest upcoming transition anywhere on the site.
One scheduled page therefore expires the whole site's page cache at that moment.
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.
Related documentation
Architecture — the implementation behind this behavior
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.
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.
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.
Warning
per-content with the default dynamic timing changes nothing at all — that
combination reads only the lifetime, which per-content leaves site-wide.
Pair it with scheduler or hybrid timing, or the refindex work never runs.
Set scoping.use_refindex = 0 to skip the refindex lookup; the strategy then falls back
to the element's own pid, which is what per-page already does. The strategy also
falls back to the pid when the refindex returns nothing or the lookup throws, so a stale
sys_refindex degrades quietly rather than failing.
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.
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:
Add the task in the Scheduler backend module and note the UID it receives.
scheduler:run executes every task that is due.
To run only this one, pass its UID: scheduler:run --task=<uid>.
There is no dedicated console command for the transition check — the extension's own
commands are temporalcache:analyze, temporalcache:verify,
temporalcache:harmonize and temporalcache:list.
Each run reads its last-run timestamp from TYPO3's Registry (namespace
tx_temporalcache, key scheduler_last_run), processes every transition since then,
and stores the new timestamp.
How often that happens is the task's frequency in the Scheduler module.
Warning
A transition is only noticed on the next task run, so content appears or disappears up
to one interval late.
The very first run has no stored timestamp and therefore treats the range as starting at
epoch, which means every past transition on the site is processed once.
Two switches, each accepting dynamic or scheduler.
There is no per-table setting: a record is classified as page (table pages) or
content (every other monitored table).
timing.hybrid.pages decides two things — how page transitions are processed by the
scheduler task, and which strategy computes the cache lifetime.
timing.hybrid.content decides only how content transitions are processed by the task.
Note
Because the lifetime always follows the pages rule, pages = dynamic keeps the
per-request MIN() queries, and those queries cover the content tables too.
Hybrid with pages = dynamic does not remove query cost for content; it changes who
reacts to content transitions.
Warning
pages = scheduler combined with content = dynamic is accepted by the
configuration but does nothing for content: the lifetime is then null and the
scheduler hands content transitions to the dynamic strategy, whose
processTransition() is a no-op.
Content transitions are silently dropped in that combination.
Time harmonization
Harmonization is a data change, not runtime behavior.
temporalcache:harmonize (and the equivalent backend action) rewrite the stored
starttime/endtime values of records, moving each to its nearest configured slot.
Fewer distinct transition moments means fewer cache invalidations, whatever the scoping and
timing strategy.
Important
Enabling harmonization.enabled on its own changes no cache behavior.
It unlocks the harmonize command and the backend analysis; the reduction only happens
once the command has actually written the rounded values.
Those values are the editors' publication times, so agree the change with them first —
and preview it with temporalcache:harmonize --dry-run, since the command writes to
the database by default.
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.
The tolerance is the maximum shift the rewrite may apply, not a threshold below which
nothing happens.
A small tolerance keeps publication times close to what editors entered and harmonizes few
records; the default 3600 harmonizes anything within an hour of a slot.
Warning
harmonization.tolerance = 0 shifts nothing except timestamps that already sit
exactly on a slot.
It does not mean "no limit".
Effect
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
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.
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;
}
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)
SELECTMIN(`starttime`) AS min_transition
FROM`tt_content`WHERE`starttime` > :nowAND`deleted` = 0AND`hidden` = 0AND`pid` = :pageId
AND (`t3ver_wsid` = 0OR`t3ver_wsid`ISNULL)
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:
A table registered through TemporalMonitorRegistry gets no index from this
extension, and adds two queries to every lookup.
Ship the equivalent index with the extension that registers the table.
Only the site-wide lookup is memoized, in a request-scoped singleton keyed by timestamp,
workspace and language.
The per-page and per-content lookups are not memoized.
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.
No benchmark figures for this extension exist in this repository, so this chapter gives
no thresholds in pages, requests or milliseconds.
It describes which configuration matches which shape of site, and what to measure on
your own installation before deciding.
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
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
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.
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.
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.
Note
The lifetime calculation always follows the pages rule, and that calculation covers
the content tables as well.
pages = dynamic therefore keeps the per-request queries — hybrid changes who reacts
to content transitions, not the query cost.
Warning
Do not configure pages = scheduler together with content = dynamic.
Content transitions are silently dropped in that combination; see
A hybrid combination that drops transitions.
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
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.
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.
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.
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.
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.
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
Decision guide — choosing a configuration for this extension
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:
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.
Warning
Switching to per-content does not help here.
That strategy narrows flush tags, not lifetimes, and flush tags are only read by
scheduler and hybrid timing.
With the default dynamic timing it behaves exactly like global.
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:
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.
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.
Warning
With scheduler or hybrid timing this does not hold.
The scheduler task looks for transitions in the live workspace and the default language
only, so transitions on workspace versions and on translated records are not processed.
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.
Extract to typo3conf/ext/nr_temporal_cache/ (classic mode) or
packages/nr_temporal_cache/ (Composer mode)
Activate in the Extension Manager
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):
Alternative: For ext_localconf.php (when DI not available):
<?phpuseNetresearch\TemporalCache\Service\TemporalMonitorRegistry;
useTYPO3\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:
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
Create a test page:
Set Start to 5 minutes in the future
Enable In menu
Save
Check the frontend menu — the page must not appear yet
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
Create a content element with Stop 5 minutes in the future
View the page — the element is visible
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
Confirm the extension is loaded:
vendor/bin/typo3 extension:list
Copied!
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.
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.
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.
Important
The choice of scoping and timing strategy changes what the extension does,
not only how fast it does it.
Read How the settings combine before changing
either, and Performance considerations before deploying the change.
Chapters
🎯 Optimization strategies
Scoping, timing and harmonization settings, and how the three interact.
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.
Note
The wizard does not write configuration.
It shows which values to use, as its own note says; apply them in the
Extension Manager or in additional.php.
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.
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.
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.
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.
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
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.
Note
The configuration key is pages, the content type it maps to is
page.
ExtensionConfiguration::getTimingRules() performs that mapping, so
the rule reaches HybridTimingStrategy, which looks rules up by
TemporalContent::getContentType().
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.
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.
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
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.
Warning
0 does not mean "no limit".
With a tolerance of 0 only a timestamp that already sits exactly on a
slot passes the check, so harmonization changes nothing at all.
The label in ext_conf_template.txt still reads 0 = no limit
and is wrong.
temporalcache:verify treats a tolerance outside 1-86400 as
invalid.
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
Declares that harmonized times should be suggested while editing.
Note
The value is read and reported — by the Reports module status provider,
by temporalcache:analyze and by temporalcache:verify —
but no backend form acts on it in this version.
Harmonization suggestions are shown in the Content tab of the backend
module, which is gated by harmonization.enabled, not by this
setting.
Upper bound in seconds for the cache lifetime this extension calculates.
Two places read it:
DynamicTimingStrategy::getCacheLifetime() returns this value when
no transition is scheduled at all, and caps the calculated lifetime at it
otherwise.
TemporalCacheLifetime caps the value it finally writes to the
event, using the hierarchy below.
Configuration hierarchy
TemporalCacheLifetime::determineMaxLifetime() takes the first value
that applies:
TypoScript config.cache_period, when it is set and greater than 0
This setting, when it is greater than 0
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.
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.
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!
Warning
Enable only for debugging.
The listener writes one entry per page cache generation.
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.
Note
Registering the task type is not the same as scheduling it.
Until a task is created and runs,
timing.strategy = scheduler leaves the page cache lifetime untouched and
has nothing that flushes it; hybrid still calculates a lifetime for
whichever content type is routed to dynamic, but the transitions routed
to the scheduler wait for the task; and the targeted cache tags of
per-page and per-content scoping stay unused, because only the task
triggers them.
timing.strategy = dynamic needs no task at all.
Creating the task is not enough on its own either — the Scheduler needs a cron
entry that runs it:
Every example that sets timing.strategy to scheduler, or routes a
content type to the scheduler through hybrid, depends on the scheduler
task.
Read Scheduler task first: the task type is registered, but a task
still has to be created in the Scheduler module and run by cron before those
transitions are processed.
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.
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.
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.
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.
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.
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.
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.
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
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.
Do the indexes exist?
The extension ships them in ext_tables.sql; TYPO3 creates them
during the database compare, not at installation time.
SHOWINDEXFROM pages WHERE Key_name LIKE'idx_temporalcache%';
SHOWINDEXFROM 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.
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.
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
Make sure the indexes above exist — without them these are full table scans.
Set timing.strategy = scheduler to remove the queries from page
generation entirely.
This needs the scheduler task; read Scheduler task first.
Check the query plan:
EXPLAINSELECTMIN(starttime) FROM pages
WHERE starttime > UNIX_TIMESTAMP()
AND hidden = 0AND deleted = 0AND 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
harmonization.enabled = 1.
While it is off, the service returns every timestamp unchanged and the
backend module hides the column.
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.
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.
The distance is measured within the day.
23:30 is far from a 00:00 slot, not close to the next day's.
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:
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.
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 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.
Note
The Transitions card counts 30 days, the timeline below it covers 7 days.
The two figures are expected to differ.
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.
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.
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.
Harmonize Selected becomes active as soon as one record is selected.
Confirm the dialog.
The selected start and end times are written to the database, the page cache group is flushed, and the view
reloads.
Warning
Harmonization writes the new timestamps directly, bypassing the DataHandler.
No sys_history entry is created and the change cannot be undone from the backend.
Try temporalcache:harmonize --dry-run first to see the full list of pending changes.
Guided walkthrough of the available strategies, with three ready-made preset combinations.
Important
The wizard does not write any configuration.
Every "Apply" button ends in a notification that points at
Admin Tools > Settings > Extension Configuration > nr_temporal_cache, where the settings have to
be entered by hand.
See Configuration for the settings themselves.
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.
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
Run vendor/bin/typo3 temporalcache:verify and fix everything it reports.
Check the timing strategy on the dashboard.
With the scheduler or hybrid strategy, confirm that the scheduler task runs — see
Scheduler task.
Confirm that the affected record is listed at all, with
vendor/bin/typo3 temporalcache:list --upcoming.
No harmonization suggestions appear
Harmonization must be enabled in the extension configuration — the suggestion column is not rendered
otherwise.
Check slots and tolerance with vendor/bin/typo3 temporalcache:verify, which validates the slot format
and the tolerance range.
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:
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.
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
Note
The statistics table is resolved per workspace only.
The --language value is applied to the transition analysis and the harmonization impact, not to the
content counts.
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
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; thenecho"Temporal cache system healthy"elseecho"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:
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
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.
Warning
The command writes the new timestamps directly through the database connection, bypassing the DataHandler.
No sys_history entry is created and the change cannot be undone from the backend.
Each write is recorded in the TYPO3 log instead.
Run with --dry-run and take a database backup first.
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
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.
Prints the harmonization context: mode, workspace, language, table filter, configured time slots and
tolerance.
Loads the temporal content of the selected workspace and language and applies the table filter.
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.
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.
Applies the changes, reports how many records were updated and how many failed, and flushes the pages
cache group.
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.
The Temporal Cache extension contributes five status entries to TYPO3's built-in Reports module.
Accessing the report
Log in to the TYPO3 backend as an administrator
Navigate to: Admin Tools > Reports > Status Report
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:
Navigate to: Admin Tools > Settings > Extension Configuration
Locate temporal_cache in the list
Review and adjust the settings based on recommendations
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:
Navigate to: Admin Tools > Maintenance > Analyze Database Structure
Review the proposed changes
Apply the schema updates to create missing indexes
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):
Consider enabling harmonization to group transitions
Evaluate if scheduler-based timing would be more efficient
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:
Run database schema update to create indexes
Start adding temporal content (pages/content with starttime/endtime)
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:
Immediate: Create missing database indexes
Short-term: Enable harmonization to reduce cache churn
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:
No pages or content elements have starttime/endtime set
Content exists but is in a different workspace
Content is in a different language
Actions:
Verify temporal content exists in the backend
Check if you're viewing the correct workspace (report shows live workspace by default)
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:
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.shcd /var/www/html/typo3
vendor/bin/typo3 temporalcache:verify >/dev/null 2>&1
if [ $? -eq 0 ]; thenecho"OK - Temporal Cache system healthy"exit 0
elseecho"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:
Extension not installed or activated
Cache not cleared after installation
Missing service registration
Solutions:
Verify extension is installed: Admin Tools > Extensions
Clear all caches: Admin Tools > Maintenance > Flush Cache
Check if Services.yaml is properly loaded (check system log)
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)
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:
cache_period from the rendering instructions, if it is set and greater than zero
(TypoScript config.cache_period)
advanced.default_max_lifetime from the extension configuration, if greater than zero
86400
The listener also caps the value the timing strategy already capped, because
DynamicTimingStrategy limits its own result to advanced.default_max_lifetime
independently.
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:
The configured name is scoping.strategy respectively timing.strategy from the
extension configuration.
Three consequences follow from this code:
The tags carry noidentifier 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:
Setting scoping.strategy to rootline then activates it.
Note
Inside this extension, autoregistration excludes Service/Scoping/*Strategy.php and
Service/Timing/*Strategy.php, so its own strategies need explicit service
definitions.
An extension with the default autoconfigure setup only has to add the tag.
Transition lookups
All temporal queries live in
Netresearch\TemporalCache\Domain\Repository\TemporalContentRepository
(contract: TemporalContentRepositoryInterface).
Three methods feed the strategies:
// Earliest transition across every monitored table (site-wide)publicfunctiongetNextTransition(
int $currentTimestamp,
int $workspaceUid = 0,
int $languageUid = 0
): ?int;
// Earliest transition in the pages table onlypublicfunctiongetNextPageTransition(
int $currentTimestamp,
int $workspaceUid = 0,
int $languageUid = 0
): ?int;
// Earliest transition in the content tables, restricted to one pidpublicfunctiongetNextContentTransitionForPage(
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.
Important
per-content narrows the flush tags, not the cache lifetime.
Its getNextTransition() deliberately returns the site-wide transition, because a
content element can be embedded into arbitrary pages and a narrowed lifetime would risk
serving stale embedded content.
Combined with dynamic timing — which only ever reads the lifetime — per-content
therefore behaves exactly like global.
Its precision takes effect with scheduler or hybrid timing.
Note
per-page keeps menus correct by watching all page transitions site-wide, but it does
not see content embedded from another page through CONTENT/RECORDS cObjects.
That page's lifetime is not shortened when the embedded element transitions.
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.
Note
HybridTimingStrategy::getCacheLifetime() always uses the pages rule; it cannot
tell during page generation which content elements a page contains.
With the default pages = dynamic the lifetime calculation therefore still queries
the content tables through the scoping strategy.
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:
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:
The second argument is optional.
Omitting it applies the default field list, which is the one shown above.
Note
The registry has no field mapping: the table's temporal columns must literally be named
starttime and endtime.
registerTable() throws an InvalidArgumentException when the field list omits
uid, starttime or endtime, when the table name is empty, or when it is
pages or tt_content — both are monitored by default and cannot be re-registered.
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
Note
TemporalCacheLifetime is final and cannot be extended.
Register your own listener instead.
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.
ext_emconf.php declares version 0.9.0 and state beta.
v0.9.0 is the first stable release.
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.
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:
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.
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.