Skip to content

Stale-While-Revalidate (SWR) ​

SWR keeps public presentationItem pages fast when the fresh TTL expires. The active store is chosen by MODULAROUS_PRESENTATION_CACHE_STORE (url, model, or none).

Full env table and layer resolution: Configuration — Presentation item cache.

Storage model ​

StoreDriverPurpose
url (default)UrlKeyedStaleCacheLocale + normalized path; optional middleware serve_first
modelStaleFileCache (id-keyed)Legacy id/locale keyed files under modularous-stale — requires store=model
none—Always render; no presentation cache

When presentationItem.swr is true and the route has types.presentationItem=true, the active store serves HTML past fresh TTL until stale_expires_at (controller path). Background warm jobs are dispatched with cooldown MODULAROUS_RESOURCE_CACHE_SWR_WARM_COOLDOWN.

store=model vs store=url (important) ​

These are mutually exclusive store strategies, not layered fallbacks:

Aspectstore=urlstore=model
File layoutmodularous-stale-by-url/{locale}/{hash}.htmlmodularous-stale/{Module}/{Route}/{Model}/{id}/{locale}/…
KeyLocale + path (+ optional query)Module + route + model id + locale
Writes on missAlways (when route enabled)Only when swr=true
serve_first middlewareAvailableNot used
Redis required for readsNo (isCacheTypeConfigured)Yes (isEnabled)
SWR status headerURL_STALESTALE

The deprecated cache.swr.* config aliases still resolve for tests, but new deployments should set MODULAROUS_PRESENTATION_CACHE_STORE and MODULAROUS_PRESENTATION_CACHE_SWR directly.

URL files:

storage/framework/cache/modularous-stale-by-url/{locale}/{sha256(path)}.html

Model store:

storage/framework/cache/modularous-stale/{Module}/{Route}/{Model}/{id}/{locale}/{paramsHash}.html

Public presentationItem does not use Redis for HTML storage.

Configuration ​

php
'presentationItem' => [
    'store' => env('MODULAROUS_PRESENTATION_CACHE_STORE', 'url'), // url | model | none
    'swr' => env('MODULAROUS_PRESENTATION_CACHE_SWR', false),
    'serve_first' => env('MODULAROUS_PRESENTATION_CACHE_SERVE_FIRST', true),
    'stale_ttl' => (int) env('MODULAROUS_PRESENTATION_CACHE_STALE_TTL', 604800),
    'warm_dispatch_cooldown' => (int) env('MODULAROUS_RESOURCE_CACHE_SWR_WARM_COOLDOWN', 600),
    'model' => ['stale_path' => env('MODULAROUS_RESOURCE_CACHE_SWR_STALE_PATH', storage_path('framework/cache/modularous-stale'))],
    'url' => [
        'driver' => env('MODULAROUS_PRESENTATION_CACHE_URL_DRIVER', 'file'),
        'base_path' => env('MODULAROUS_CACHE_URL_STALE_PATH', storage_path('framework/cache/modularous-stale-by-url')),
    ],
],
Env varConfig keyDefaultMeaning
MODULAROUS_PRESENTATION_CACHE_STOREpresentationItem.storeurlurl = path files; model = id-based stale (legacy SWR path)
MODULAROUS_PRESENTATION_CACHE_SWRpresentationItem.swrfalseEnable stale window + background warm in controller
MODULAROUS_PRESENTATION_CACHE_STALE_TTLpresentationItem.stale_ttl604800stale_expires_at — hard file expiry from write time
MODULAROUS_RESOURCE_CACHE_TTL_PRESENTATION_ITEMttl.presentationItem900Fresh TTL → expires_at in URL .meta
MODULAROUS_RESOURCE_CACHE_SWR_WARM_COOLDOWNpresentationItem.warm_dispatch_cooldown600Lock TTL before re-dispatching WarmPresentationItemJob
MODULAROUS_RESOURCE_CACHE_SWR_STALE_PATHpresentationItem.model.stale_pathstorage/framework/cache/modularous-staleModel store directory (store=model only)

Legacy: MODULAROUS_RESOURCE_CACHE_SWR_ENABLED → presentationItem.swr when MODULAROUS_PRESENTATION_CACHE_SWR is unset. MODULAROUS_RESOURCE_CACHE_SWR_STALE_TTL → stale_ttl.

SWR applies per route only when types.presentationItem is enabled in module cache config.

SWR off vs on (by store) ​

Storeswr=falseswr=true
urlController re-renders after fresh TTL (MISS). serve_first middleware may still return URL_STALE until stale_expires_at.Controller returns URL_STALE + queues warm.
modelNo stale files written; always render.Serves id-keyed stale file (STALE) + queues warm.
noneNo effectNo effect

Request flow ​

mermaid
sequenceDiagram
    participant Browser
    participant MW as ServeUrlKeyedStaleMiddleware
    participant Cms as CmsController
    participant PIC as CmsPublicPresentationItemCache
    participant UrlFile as UrlKeyedStaleCache
    participant IdFile as StaleFileCache
    participant Queue as WarmPresentationItemJob

    Browser->>MW: GET public page
    alt URL file HIT (serve_first)
        MW->>UrlFile: get
        UrlFile-->>Browser: HTML URL_HIT
    else middleware miss
        MW->>Cms: continue
        Cms->>PIC: resolvePresentationHtml()
        PIC->>UrlFile: get
        alt URL fresh HIT
            UrlFile-->>PIC: HTML
            PIC-->>Cms: URL_HIT
        else URL stale + SWR (store=url)
            UrlFile-->>PIC: HTML
            PIC->>Queue: dispatch warm
            PIC-->>Cms: URL_STALE
        else id-based stale (store=model)
            PIC->>IdFile: get stale
            PIC-->>Cms: STALE
        else render
            PIC->>PIC: render HTML
            PIC->>PIC: put active store
            PIC-->>Cms: MISS
        end
    end
    Cms-->>Browser: HTML + X-Modularous-Cache header

Response header ​

Public CMS responses include:

X-Modularous-Cache: presentationItem=URL_HIT|URL_STALE|STALE|MISS|BYPASS|DISABLED
StatusWhen
URL_HITURL file served, now <= expires_at
URL_STALEURL file served past fresh TTL (middleware always; controller when swr=true)
STALEId-based StaleFileCache hit (store=model, swr=true)
MISSRendered live; file written if store allows
BYPASSExplicit SWR bypass (e.g. preview)
DISABLEDstore=none or route/global cache off

Use this header in monitoring to distinguish URL disk hits from id-based stale serves.

Resilience ​

ServeUrlKeyedStaleMiddleware can serve URL-keyed HTML before the controller (no DB/Redis). See URL Stale Resilience.

Render failures are not masked — exceptions from Blade rendering still propagate.

Invalidation ​

ActionURL stale filesId-based staleRedis admin types
Model save / relation tag flushDeleted via UrlRoute pathsDeleted via forgetByRelationFlushed via tags
Explicit per-record purgeDeletedDeletedFlushed
Fresh TTL expiryServes as URL_STALE until stale_expires_atServes as STALE (fallback)N/A for public HTML

Revalidate webhook ​

External systems (CDN, deploy hooks) can queue purge/warm via HMAC-signed POST:

POST /api/modularous/cache/revalidate
X-Modularous-Signature: sha256=<hmac of raw body>
json
{
  "module": "PrimaryPage",
  "route": "Home",
  "id": 42,
  "action": "purge",
  "types": ["presentationItem"]
}
FieldRequiredValues
moduleyesStudlyCase module name
routeyesStudlyCase route name
idnoRecord id (omit for route-wide)
actionyespurge, warm, both
typesnoDefaults to presentationItem only

Webhook config:

php
'webhook' => [
    'enabled' => env('MODULAROUS_RESOURCE_CACHE_WEBHOOK_ENABLED', false),
    'secret' => env('MODULAROUS_RESOURCE_CACHE_WEBHOOK_SECRET'),
],

Disabled by default. Returns 404 when disabled; 401 on signature mismatch.