{"id":15552,"date":"2026-09-05T13:43:40","date_gmt":"2026-09-05T11:43:40","guid":{"rendered":"https:\/\/www.lukaswojcik.com\/blog\/toolbox\/cache-control-revalidation-analyzer\/"},"modified":"2026-09-05T13:43:40","modified_gmt":"2026-09-05T11:43:40","slug":"cache-control-revalidation-analyzer","status":"publish","type":"page","link":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/cache-control-revalidation-analyzer\/","title":{"rendered":"Cache-Control &#038; Revalidation Analyzer"},"content":{"rendered":"<div class=\"gtm-analyser-container\" style=\"background: var(--bg-panel, #1e1e24); padding: 25px; border-radius: 8px; border: 1px solid var(--border, #2a2a35);\">\n<p style=\"color: var(--text-secondary, #a0a0b0); margin-bottom: 20px;\">Cache-Control is a short header with a long list of ways to misread it: no-cache does not mean do not store, max-age=0 is not no-store, s-maxage only speaks to shared caches, and without any of them a browser invents a lifetime from Last-Modified. This check fetches an address from this server, computes the freshness lifetime for a browser and for a shared cache the way RFC 9111 does, then sends a request with If-None-Match and one with If-Modified-Since to see whether the validators actually work, and a second plain request to read what a cache in front does the second time.<\/p>\n<div style=\"margin-bottom: 14px;\">\n        <label for=\"cc-url\" style=\"color: var(--text-secondary, #a0a0b0); display: block; font-size: 0.85rem; margin-bottom: 5px;\">Address (a page, a stylesheet, a script, an image)<\/label><br \/>\n        <input id=\"cc-url\" type=\"text\" value=\"https:\/\/www.wikipedia.org\/\" class=\"form-control\" style=\"width: 100%; padding: 10px; background: var(--bg-body, #14141a); border: 1px solid var(--border, #2a2a35); color: var(--text-primary, #e8e8ee); border-radius: 6px; box-sizing: border-box; font-family: monospace;\">\n    <\/div>\n<p style=\"color: var(--text-secondary, #a0a0b0); font-size: 0.82rem; margin-bottom: 16px;\">Redirects and up to four requests of at most 256 KB each go from this server to the address entered. Hosts in private, loopback and link-local networks are refused. The query is protected by reCAPTCHA v3; the calling IP address and the target are stored for one hour to limit the rate.<\/p>\n<p>    <button id=\"cc-btn\" class=\"button\" style=\"background: var(--accent, #7ee787); color: var(--on-accent, #0b1114); border: none; padding: 12px 24px; border-radius: 6px; font-weight: 700; cursor: pointer;\">Analyse caching headers<\/button><\/p>\n<div id=\"cc-ausgabe\" style=\"margin-top: 22px;\"><\/div>\n<p style=\"color: var(--text-secondary, #a0a0b0); font-size: 0.8rem; margin: 24px 0 0;\">Limits worth knowing: only one of the resolved addresses is tested; a CDN answers per edge, so the cache status seen from here may differ elsewhere; the second request follows the first within seconds, which a cache with a minimum TTL may treat differently from real traffic; headers set by JavaScript or Service Workers are not visible; and the heuristic lifetime is the RFC suggestion of ten percent, while individual browsers use their own rules.<\/p>\n<\/div>\n<p><script>\n(function () {\n    'use strict';<\/p>\n<p>    const T = {\"einleitung\":\"Cache-Control is a short header with a long list of ways to misread it: no-cache does not mean do not store, max-age=0 is not no-store, s-maxage only speaks to shared caches, and without any of them a browser invents a lifetime from Last-Modified. This check fetches an address from this server, computes the freshness lifetime for a browser and for a shared cache the way RFC 9111 does, then sends a request with If-None-Match and one with If-Modified-Since to see whether the validators actually work, and a second plain request to read what a cache in front does the second time.\",\"label_url\":\"Address (a page, a stylesheet, a script, an image)\",\"datenschutz\":\"Redirects and up to four requests of at most 256 KB each go from this server to the address entered. Hosts in private, loopback and link-local networks are refused. The query is protected by reCAPTCHA v3; the calling IP address and the target are stored for one hour to limit the rate.\",\"knopf\":\"Analyse caching headers\",\"laeuft\":\"Fetching, then revalidating...\",\"grenze\":\"Limits worth knowing: only one of the resolved addresses is tested; a CDN answers per edge, so the cache status seen from here may differ elsewhere; the second request follows the first within seconds, which a cache with a minimum TTL may treat differently from real traffic; headers set by JavaScript or Service Workers are not visible; and the heuristic lifetime is the RFC suggestion of ten percent, while individual browsers use their own rules.\",\"h_verhalten\":\"What a cache does with it\",\"zeile_url\":\"Measured address\",\"zeile_ip\":\"Resolved addresses\",\"zeile_klasse\":\"Resource\",\"zeile_verhalten\":\"Browser behaviour\",\"zeile_browser\":\"Fresh in the browser for\",\"zeile_geteilt\":\"Fresh in a shared cache for\",\"zeile_dauer\":\"Took\",\"zeile_limit\":\"Checks left this hour\",\"versioniert_zusatz\":\"(versioned file name)\",\"klasse_html\":\"HTML document\",\"klasse_asset\":\"stylesheet or script\",\"klasse_bild\":\"image\",\"klasse_schrift\":\"font\",\"klasse_daten\":\"data (JSON, XML, text)\",\"klasse_sonstiges\":\"other\",\"verhalten_nicht_gespeichert\":\"Not stored at all: every visit downloads the response again.\",\"verhalten_heuristisch\":\"No lifetime given and no validator: the browser guesses a lifetime from Last-Modified and, without one, keeps nothing.\",\"verhalten_heuristisch_oder_revalidiert\":\"No explicit lifetime: the browser guesses one from Last-Modified and revalidates afterwards.\",\"verhalten_immer_revalidiert\":\"Stored, but every use asks the server first; unchanged content comes back as a short 304.\",\"verhalten_immer_neu\":\"Stored, but every use asks the server first, and without a validator the server has to send everything again.\",\"verhalten_unveraenderlich\":\"Stored for the whole lifetime and never revalidated, not even on reload.\",\"verhalten_frisch_dann_revalidiert\":\"Served from the cache during the lifetime, then revalidated with a conditional request.\",\"verhalten_frisch_dann_neu\":\"Served from the cache during the lifetime, then downloaded again in full, because there is no validator.\",\"verhalten_unbekannt\":\"Not determinable, the address could not be fetched.\",\"quelle_no_store\":\"no-store, never stored\",\"quelle_private\":\"private, not for shared caches\",\"quelle_s_maxage\":\"from s-maxage\",\"quelle_max_age\":\"from max-age\",\"quelle_expires\":\"from Expires\",\"quelle_expires_unlesbar\":\"Expires unreadable, counts as expired\",\"quelle_heuristisch\":\"heuristic, 10 % of the age since Last-Modified\",\"quelle_keine\":\"no information\",\"h_kopf\":\"Caching headers as sent\",\"h_abrufe\":\"Requests\",\"spalte_abruf\":\"Request\",\"spalte_status\":\"Status\",\"spalte_bytes\":\"Bytes\",\"spalte_ttfb\":\"First byte\",\"spalte_age\":\"Age\",\"spalte_cache\":\"Cache status\",\"abruf_erster\":\"Plain GET\",\"abruf_inm\":\"With If-None-Match (ETag)\",\"abruf_ims\":\"With If-Modified-Since\",\"abruf_zweiter\":\"Plain GET, second time\",\"abruf_entfaellt\":\"not sent, the first response carried no validator for it\",\"fehlgeschlagen\":\"failed\",\"h_befunde\":\"Findings\",\"stufe_hoch\":\"High\",\"stufe_mittel\":\"Medium\",\"stufe_niedrig\":\"Low\",\"stufe_info\":\"Information\",\"stufe_gut\":\"In order\",\"keine_befunde\":\"Nothing to report.\",\"urteil_gut\":\"Lifetime and revalidation are consistent for this kind of resource.\",\"urteil_warnung\":\"Caching works in principle, with something that costs downloads or confuses caches.\",\"urteil_kritisch\":\"The address could not be fetched.\",\"fehler_kein_token\":\"The check could not be started because reCAPTCHA did not load.\",\"fehler_captcha\":\"reCAPTCHA classified the request as automated. Reloading the page normally helps.\",\"fehler_zu_viele\":\"The limit of {limit} checks per hour for this address has been reached.\",\"fehler_adresse\":\"That address cannot be used.\",\"fehler_zu_viele_spruenge\":\"The address redirects more than five times; the chain was not followed further.\",\"fehler_zaehler\":\"The rate counter is unavailable, so nothing was checked.\",\"fehler_pruefdienst\":\"The reCAPTCHA service could not be reached.\",\"fehler_aufbau\":\"The service is not configured correctly. The fault is on this side.\",\"fehler_eingabe\":\"The request could not be read.\",\"fehler_methode\":\"Wrong request method.\",\"fehler_netz\":\"The service could not be reached.\",\"fehler_antwort\":\"The answer could not be read.\",\"fehler_unbekannt\":\"Something went wrong that has no message of its own.\",\"grund_leer\":\"Nothing was entered.\",\"grund_zu_lang\":\"The input is too long.\",\"grund_unlesbar\":\"It does not parse as an address.\",\"grund_schema\":\"Only http and https addresses are checked.\",\"grund_benutzerinfo\":\"Inputs with credentials in them are refused.\",\"grund_kein_punkt\":\"The host name needs at least one dot.\",\"grund_hostform\":\"Enter a host name, not an IP address.\",\"grund_port\":\"Only the ports 80, 443, 8080 and 8443 are checked.\",\"grund_gesperrter_bereich\":\"That host resolves into a private or reserved network. Those are never contacted.\",\"grund_kein_dns\":\"The host name does not resolve.\",\"grund_abruf\":\"The address did not answer.\",\"befund_weiterleitung\":\"Redirected {n} time(s) to {ziel} || The analysis refers to the final address of the chain.\",\"befund_abruf_fehlgeschlagen\":\"The address does not answer || {fehler}\",\"befund_status_fehler\":\"The server answered with status {status} || The caching headers of an error response are analysed, not those of the intended resource. Error responses are rarely cached, and 404s are cacheable only when they say so.\",\"befund_heuristisch\":\"No Cache-Control, no Expires: lifetime guessed from Last-Modified ({dauer}) || Without an explicit lifetime a cache may use a heuristic; RFC 9111 suggests ten percent of the time since Last-Modified ({lm}). That makes the lifetime grow with the age of the file and differ between browsers. An explicit max-age replaces the guess.\",\"befund_kein_cache_control\":\"No Cache-Control and no Expires || Nothing says how long the response may be reused, and without Last-Modified a cache cannot even guess. Browsers then revalidate or refetch every time. A max-age suited to the resource is the fix.\",\"befund_no_store\":\"no-store || The response must not be written to any cache. Right for personal or sensitive content, wasteful for anything static: every visit downloads it again, and the back button reloads it too.\",\"befund_no_cache\":\"no-cache || Often misread: the response may be stored, but every reuse must be checked with the server first. With a validator that is a cheap 304; without one it is a full download. To forbid storing, no-store is the directive.\",\"befund_no_store_und_no_cache\":\"no-store together with no-cache || no-store already forbids storing; no-cache on top is redundant. Harmless, but it suggests the directives were copied rather than chosen.\",\"befund_max_age_null\":\"max-age=0 || Fresh for zero seconds, so every reuse revalidates. Unlike no-store, the response is kept and can be confirmed with a 304 ({revalidate}). This is the classic setting for HTML that must always be current but is mostly unchanged.\",\"befund_private\":\"private || Only the browser may store the response; shared caches and CDNs must not. Correct for personalised pages, a lost opportunity for public ones.\",\"befund_public_ohne_dauer\":\"public without a lifetime || public allows shared caches to store the response, even one that would otherwise be private, but says nothing about how long. On its own it rarely does what was intended.\",\"befund_s_maxage\":\"s-maxage {dauer} for shared caches, {browser} for browsers || Shared caches such as a CDN use s-maxage and ignore max-age; browsers ignore s-maxage. Two lifetimes for two audiences, which is exactly the point of the directive.\",\"befund_immutable_kurz\":\"immutable with a short lifetime ({dauer}) || immutable tells the browser not to revalidate even on reload. That only makes sense with a long lifetime and a versioned file name; with a short one it saves almost nothing.\",\"befund_immutable_ohne_version\":\"immutable on a file name without a version || The browser will not revalidate during the lifetime, not even on reload. If the file changes under the same name, visitors keep the old one until it expires. immutable belongs on hashed or versioned file names.\",\"befund_immutable_ok\":\"immutable with {dauer} on a versioned file name || The browser keeps the file for the whole lifetime and skips the conditional request on reload. This is the intended use.\",\"befund_swr\":\"stale-while-revalidate {dauer} || After the lifetime, the cache may serve the stale copy for this long while it fetches a fresh one in the background. Visitors never wait for the revalidation.\",\"befund_sie\":\"stale-if-error {dauer} || If the origin fails, the cache may keep serving the stale copy for this long. A quiet safety net during outages.\",\"befund_must_revalidate\":\"must-revalidate with a lifetime of {dauer} || Once stale, the response must not be served without a successful revalidation, not even when the origin is unreachable. It does not shorten the lifetime; it only forbids serving stale.\",\"befund_pragma\":\"Pragma: no-cache || An HTTP\\\/1.0 relic. As a response header it has no defined meaning; caches follow Cache-Control. Harmless, but it does nothing.\",\"befund_direktive_unbekannt\":\"Unknown directives: {liste} || Caches ignore directives they do not know. Typos here silently disable what was intended.\",\"befund_expires_widerspruch\":\"Expires ({expires}) disagrees with max-age ({maxage}) || When both are present, max-age wins and Expires is ignored. A mismatch usually means two layers set them independently; the Expires value is dead weight.\",\"befund_expires_unlesbar\":\"Expires is not a valid date ({wert}) || An unreadable Expires counts as already expired. Values like 0 or -1 are a common way to say that, but a Cache-Control directive says it more clearly.\",\"befund_expires_vergangen\":\"Expires lies in the past || The response is stale on arrival and will be revalidated or refetched on every use. Without max-age, this is the only lifetime information.\",\"befund_expires_allein\":\"Lifetime only from Expires ({dauer}) || Works, but Expires is an absolute time and depends on the clocks of server and client agreeing. max-age is relative and immune to that.\",\"befund_date_abweichung\":\"Server clock differs from this server\\u0027s by {sekunden} s || Expires and Age computations depend on the Date header. A drifting server clock shortens or lengthens every lifetime computed from absolute times.\",\"befund_date_fehlt\":\"No Date header || Origin servers must send one. Without it, caches use their own clock, and Expires cannot be interpreted reliably.\",\"befund_validator_fehlt\":\"No ETag and no Last-Modified || After the lifetime, the browser cannot ask whether the content changed; it has to download everything again. A Last-Modified or an ETag turns that into a 304 with no body.\",\"befund_etag_schwach\":\"Weak ETag ({etag}) || W\\\/ marks a validator that promises semantic, not byte-for-byte, equivalence. Fine for revalidation; not usable for range requests.\",\"befund_last_modified_unlesbar\":\"Last-Modified is not a valid date ({wert}) || Clients ignore an unreadable validator, so conditional requests based on it never happen.\",\"befund_last_modified_zukunft\":\"Last-Modified lies in the future ({wert}) || A modification time later than now points at a wrong server clock. Caches treat it as the current time; heuristics based on it collapse.\",\"befund_etag_304\":\"If-None-Match answered 304 || The ETag works: an unchanged resource costs a few header bytes instead of {bytes} bytes.\",\"befund_etag_kein_304\":\"If-None-Match answered 200, not 304 || The server sent an ETag ({etag}) but did not recognise it in a conditional request; the whole body came again. Apache with mod_deflate is a classic cause: it appends -gzip to the ETag and then fails to match its own suffix (DeflateAlterETag). CDNs that rewrite ETags do the same.\",\"befund_etag_status\":\"If-None-Match answered {status} || Neither 200 nor 304; the conditional request was treated as something else.\",\"befund_ims_304\":\"If-Modified-Since answered 304 || The date-based validator works.\",\"befund_ims_kein_304\":\"If-Modified-Since answered 200, not 304 || The server sends Last-Modified but does not honour it in a conditional request. With a working ETag this costs nothing; without one, every revalidation is a full download.\",\"befund_304_mit_inhalt\":\"304 with a body of {bytes} bytes || A 304 must not carry a body. Clients ignore it, but the bytes are still transferred, and some intermediaries choke on it.\",\"befund_etag_wechselt\":\"ETag changed between two identical requests || The first response carried {a}, the conditional one {b}. A validator that changes on every request never matches, so conditional requests always fail. Typical causes: an ETag built from a timestamp or from a per-request token.\",\"befund_vary_stern\":\"Vary: * || The response varies on something outside the request headers; caches must treat every request as different. Effectively uncacheable by shared caches.\",\"befund_vary_user_agent\":\"Vary: User-Agent || With thousands of distinct User-Agent strings, a shared cache keeps a copy per string and almost never hits. Client hints or no Vary at all serve the same purpose without the fragmentation.\",\"befund_vary_cookie\":\"Vary: Cookie || Every distinct cookie value gets its own cache entry. If the page also sets a per-visitor cookie, the cache never hits for a second visitor.\",\"befund_set_cookie_geteilt\":\"Set-Cookie on a response that shared caches may keep for {dauer} || A cache that stores this response stores the cookie with it and hands one visitor\\u0027s cookie to the next. Most CDNs refuse to cache responses with Set-Cookie for that reason, so the intended s-maxage does not happen either. Either drop the cookie or mark the response private.\",\"befund_cache_davor\":\"A cache sits in front of the origin ({system}) || First request: {status1}, Age {age1}; second request: {status2}, Age {age2}. Age counts the seconds the copy has been in the cache.\",\"befund_cache_trifft\":\"The cache in front is hitting ({status}) || The response was served from the cache, not from the origin. Visitors get it without the origin doing any work.\",\"befund_cache_trifft_nicht\":\"The cache in front does not store this ({status}) || Both requests went to the origin. For HTML behind a CDN this is the usual default; static assets should hit.\",\"befund_age_ueber_frische\":\"Age {age} s exceeds the lifetime of {frische} s || The cache served a copy that is already stale by its own headers. Either the cache applies its own rules, or stale-while-revalidate is at work.\",\"befund_html_lang\":\"HTML cached for {dauer} without revalidation || Visitors may see an outdated page for that long after an edit, and a fix cannot be pushed to them. A short max-age with must-revalidate, or max-age=0 with a validator, keeps HTML current at low cost.\",\"befund_html_kurz\":\"HTML fresh for {dauer}, then revalidated || A short lifetime absorbs bursts of repeated requests while edits reach visitors quickly.\",\"befund_html_revalidiert\":\"HTML revalidated on every use || The browser keeps a copy and asks the server each time; unchanged pages come back as a 304. Edits reach visitors immediately.\",\"befund_asset_lang_versioniert\":\"Versioned asset cached for {dauer} || A long lifetime on a file name that changes with its content is the ideal case: cached for a year, replaced the moment the HTML references a new name.\",\"befund_asset_lang_unversioniert\":\"Long lifetime ({dauer}) on an unversioned file name || A change under the same name reaches returning visitors only when the copy expires. Either add a hash or version to the name, or shorten the lifetime.\",\"befund_asset_kurz\":\"Short lifetime for a static asset ({dauer}) || Stylesheets, scripts, images and fonts change rarely; a lifetime under a day means frequent revalidation for no benefit. With a versioned name, a year is common.\",\"befund_asset_mittel\":\"Asset cached for {dauer} || A middle ground: less revalidation than a short lifetime, still limited exposure to stale copies.\",\"befund_asset_ungecached\":\"Static asset not cached ({quelle}) || Every page view downloads this file again. A max-age, ideally with a versioned name, removes those downloads entirely.\",\"befund_versioniert_aber_kurz\":\"Versioned file name, but only {dauer} of lifetime || The name already changes with the content, so a long lifetime carries no risk. The short one gives away the benefit of the versioning.\"};<\/p>\n<p>    var ENDPUNKT = '\/lw-cachecheck.php';\n    var SITEKEY = '6LcrPkEtAAAAAPo1QCOf-IIM2fCL0UfdJz4y2iSY';\n    var STUFEN = ['hoch', 'mittel', 'niedrig', 'info', 'gut'];\n    var ABRUFE = ['erster', 'inm', 'ims', 'zweiter'];\n    var KLASSEN = ['html', 'asset', 'bild', 'schrift', 'daten', 'sonstiges'];\n    var VERHALTEN = ['nicht_gespeichert', 'heuristisch', 'heuristisch_oder_revalidiert', 'immer_revalidiert', 'immer_neu', 'unveraenderlich', 'frisch_dann_revalidiert', 'frisch_dann_neu', 'unbekannt'];\n    var QUELLEN = ['no-store', 'private', 's-maxage', 'max-age', 'expires', 'expires-unlesbar', 'heuristisch', 'keine'];\n    var KOPF_REIHE = ['cache-control', 'expires', 'date', 'last-modified', 'etag', 'age', 'vary', 'pragma', 'surrogate-control', 'cdn-cache-control', 'cloudflare-cdn-cache-control', 'cf-cache-status', 'x-cache', 'x-cache-hits', 'x-cache-enabled', 'x-served-by', 'via', 'x-varnish', 'x-litespeed-cache', 'x-nginx-cache', 'x-proxy-cache', 'x-drupal-cache', 'x-cache-status', 'set-cookie', 'content-type', 'server'];<\/p>\n<p>    function liste(w) { return Array.isArray(w) ? w : []; }\n    function zahl(w) { return (typeof w === 'number' && isFinite(w)) ? w : null; }\n    function text(schluessel, daten) {\n        var t = T[schluessel];\n        if (typeof t !== 'string') { return null; }\n        return t.replace(\/\\{([a-z_0-9]+)\\}\/g, function (m, k) {\n            return (daten && daten[k] !== undefined && daten[k] !== null) ? String(daten[k]) : m;\n        });\n    }\n    function quelleText(q) { var t = T['quelle_' + String(q || 'keine').replace(\/-\/g, '_')]; return typeof t === 'string' ? t : String(q); }<\/p>\n<p>    function abruf(r) {\n        if (!r || typeof r !== 'object') { return null; }\n        return { ok: !!r.ok, status: zahl(r.status), fehler: r.fehler ? String(r.fehler) : null, bytes: zahl(r.bytes), ttfb: zahl(r.ttfb_ms), gesamt: zahl(r.gesamt_ms), age: zahl(r.age), cacheStatus: r.cache_status ? String(r.cache_status) : null };\n    }\n    function frische(f) {\n        f = (f && typeof f === 'object') ? f : {};\n        return { sekunden: zahl(f.sekunden), quelle: QUELLEN.indexOf(f.quelle) === -1 ? 'keine' : f.quelle, text: f.text ? String(f.text) : '-' };\n    }<\/p>\n<p>    function bewerten(antwort) {\n        var a = antwort || {};\n        if (a.fehler) { return { fehler: String(a.fehler), grund: a.grund || null, limit: zahl(a.limit), kette: liste(a.kette) }; }\n        var befunde = liste(a.befunde).map(function (f) {\n            return { key: String(f.key || ''), stufe: STUFEN.indexOf(f.stufe) === -1 ? 'info' : f.stufe, daten: (f.daten && typeof f.daten === 'object') ? f.daten : {} };\n        });\n        var gruppen = {};\n        STUFEN.forEach(function (s) { gruppen[s] = befunde.filter(function (f) { return f.stufe === s; }); });\n        var abrufe = {};\n        ABRUFE.forEach(function (v) { abrufe[v] = abruf(a.abrufe ? a.abrufe[v] : null); });\n        var kopf = {};\n        if (a.kopf && typeof a.kopf === 'object') { Object.keys(a.kopf).forEach(function (k) { if (typeof a.kopf[k] === 'string') { kopf[k.toLowerCase()] = a.kopf[k]; } }); }\n        return {\n            fehler: null, url: a.url ? String(a.url) : '', host: a.host ? String(a.host) : '', ip: a.ip ? String(a.ip) : '', ips: liste(a.ips),\n            urteil: (a.urteil === 'gut' || a.urteil === 'warnung' || a.urteil === 'kritisch') ? a.urteil : 'kritisch',\n            klasse: KLASSEN.indexOf(a.klasse) === -1 ? 'sonstiges' : a.klasse, versioniert: !!a.versioniert,\n            verhalten: VERHALTEN.indexOf(a.verhalten) === -1 ? 'unbekannt' : a.verhalten,\n            browser: frische(a.browser), geteilt: frische(a.geteilt),\n            direktiven: (a.direktiven && typeof a.direktiven === 'object') ? Object.keys(a.direktiven) : [],\n            kopf: kopf, abrufe: abrufe, befunde: befunde, gruppen: gruppen,\n            kette: liste(a.kette), dauer: zahl(a.dauer_ms), limitRest: zahl(a.limit_rest)\n        };\n    }<\/p>\n<p>    function skriptLaden() {\n        return new Promise(function (auf) {\n            if (window.grecaptcha) { auf(true); return; }\n            var s = document.createElement('script');\n            s.src = 'https:\/\/www.google.com\/recaptcha\/api.js?render=' + SITEKEY;\n            s.onload = function () { auf(true); }; s.onerror = function () { auf(false); };\n            document.head.appendChild(s);\n            setTimeout(function () { auf(!!window.grecaptcha); }, 8000);\n        });\n    }\n    function tokenHolen() {\n        return skriptLaden().then(function (da) {\n            if (!da || !window.grecaptcha || !window.grecaptcha.ready) { return null; }\n            return new Promise(function (auf) {\n                var fertig = false;\n                function einmal(w) { if (!fertig) { fertig = true; auf(w); } }\n                setTimeout(function () { einmal(null); }, 12000);\n                try {\n                    window.grecaptcha.ready(function () {\n                        if (fertig) { return; }\n                        if (!window.grecaptcha.execute) { einmal(null); return; }\n                        window.grecaptcha.execute(SITEKEY, { action: 'cachecheck' }).then(function (t) { einmal(t || null); }, function () { einmal(null); });\n                    });\n                } catch (e) { einmal(null); }\n            });\n        }, function () { return null; });\n    }\n    function abfragen(url) {\n        return tokenHolen().then(function (token) {\n            return fetch(ENDPUNKT, { method: 'POST', headers: { 'Content-Type': 'application\/json' }, body: JSON.stringify({ url: url, token: token || '' }) })\n                .then(function (r) { return r.json().then(function (j) { return j; }, function () { return { fehler: 'antwort' }; }); }, function () { return { fehler: 'netz' }; });\n        });\n    }<\/p>\n<p>    window.LW_TEST = window.LW_TEST || {};\n    window.LW_TEST.CC = { bewerten: bewerten, darstellen: null, text: text, quelleText: quelleText, ENDPUNKT: ENDPUNKT, STUFEN: STUFEN, ABRUFE: ABRUFE, KLASSEN: KLASSEN, VERHALTEN: VERHALTEN, QUELLEN: QUELLEN, T: T };<\/p>\n<p>    var btn = document.getElementById('cc-btn');\n    var out = document.getElementById('cc-ausgabe');\n    if (!btn || !out) { return; }<\/p>\n<p>    function el(tag, stil, txt) { var e = document.createElement(tag); if (stil) { e.setAttribute('style', stil); } if (txt !== undefined && txt !== null) { e.textContent = txt; } return e; }\n    function wert(id) { var e = document.getElementById(id); return e ? e.value : ''; }\n    var UEBERSCHRIFT = 'font-family: \"Nunito Sans\", sans-serif; font-weight: 700; color: var(--text-primary, #e8e8ee); font-size: 0.95rem; margin: 22px 0 8px;';\n    var ZELLE = 'padding: 5px 12px 5px 0; color: var(--text-secondary, #a0a0b0); font-size: 0.87rem;';\n    var ZELLE_WERT = 'padding: 5px 12px 5px 0; color: var(--text-primary, #e8e8ee); font-size: 0.87rem; font-family: monospace; word-break: break-all;';\n    var KASTEN = 'background: var(--bg-body, #14141a); border: 1px solid var(--border, #2a2a35); border-radius: 8px; padding: 14px 16px; margin: 0 0 10px;';\n    var GUT = ' color: #7ee787;', WARN = ' color: #ffa94d;', ROT = ' color: #ff7b72;', GRAU = ' color: #8a8a99;';\n    var FARBEN = { hoch: '#ff7b72', mittel: '#ffa94d', niedrig: '#e3b341', info: '#8a8a99', gut: '#7ee787' };\n    var URTEIL = { gut: GUT, warnung: WARN, kritisch: ROT };\n    function gitter(sp) { return el('div', 'display: grid; grid-template-columns: ' + sp + '; gap: 0 18px; align-items: baseline;'); }\n    function paar(g, n, w, stil) { g.appendChild(el('div', ZELLE, n)); g.appendChild(el('div', ZELLE_WERT + (stil || ''), w)); }\n    function ms(v) { return v === null ? '-' : v + ' ms'; }<\/p>\n<p>    function darstellen(b, ziel) {\n        ziel.innerHTML = '';\n        if (b.fehler) {\n            var txt = T['fehler_' + b.fehler] || T.fehler_unbekannt;\n            if (b.grund && T['grund_' + b.grund]) { txt = txt + ' ' + T['grund_' + b.grund]; }\n            if (b.limit !== null) { txt = txt.replace('{limit}', String(b.limit)); }\n            ziel.appendChild(el('p', ZELLE + WARN + ' margin: 0;', txt));\n            return;\n        }\n        ziel.appendChild(el('div', 'font-size: 1.05rem; font-weight: 700; margin: 0 0 12px;' + URTEIL[b.urteil], T['urteil_' + b.urteil]));<\/p>\n<p>        ziel.appendChild(el('div', UEBERSCHRIFT, T.h_verhalten));\n        var g = gitter('auto auto');\n        paar(g, T.zeile_url, b.url);\n        paar(g, T.zeile_ip, b.ips.length ? b.ips.join(', ') : b.ip, GRAU);\n        paar(g, T.zeile_klasse, T['klasse_' + b.klasse] + (b.versioniert ? ' ' + T.versioniert_zusatz : ''), GRAU);\n        paar(g, T.zeile_verhalten, T['verhalten_' + b.verhalten], b.verhalten === 'unveraenderlich' || b.verhalten === 'frisch_dann_revalidiert' || b.verhalten === 'immer_revalidiert' ? GUT : (b.verhalten === 'heuristisch' || b.verhalten === 'immer_neu' ? WARN : ''));\n        paar(g, T.zeile_browser, b.browser.text + '  (' + quelleText(b.browser.quelle) + ')', b.browser.sekunden === null ? WARN : '');\n        paar(g, T.zeile_geteilt, b.geteilt.text + '  (' + quelleText(b.geteilt.quelle) + ')', b.geteilt.sekunden === null ? WARN : '');\n        if (b.dauer !== null) { paar(g, T.zeile_dauer, ms(b.dauer), GRAU); }\n        if (b.limitRest !== null) { paar(g, T.zeile_limit, String(b.limitRest), GRAU); }\n        ziel.appendChild(g);<\/p>\n<p>        var kopfKeys = KOPF_REIHE.filter(function (k) { return typeof b.kopf[k] === 'string'; });\n        if (kopfKeys.length) {\n            ziel.appendChild(el('div', UEBERSCHRIFT, T.h_kopf));\n            var gk = gitter('auto auto');\n            kopfKeys.forEach(function (k) { paar(gk, k, b.kopf[k], k === 'cache-control' ? '' : GRAU); });\n            ziel.appendChild(gk);\n        }<\/p>\n<p>        ziel.appendChild(el('div', UEBERSCHRIFT, T.h_abrufe));\n        var huelle = el('div', 'overflow-x: auto;');\n        var gp = gitter('auto auto auto auto auto auto');\n        [T.spalte_abruf, T.spalte_status, T.spalte_bytes, T.spalte_ttfb, T.spalte_age, T.spalte_cache].forEach(function (s) { gp.appendChild(el('div', ZELLE + ' font-weight: 700; color: var(--text-primary, #e8e8ee); white-space: nowrap;', s)); });\n        ABRUFE.forEach(function (v) {\n            var r = b.abrufe[v];\n            gp.appendChild(el('div', ZELLE + ' white-space: nowrap;', T['abruf_' + v]));\n            if (r === null) { gp.appendChild(el('div', ZELLE_WERT + GRAU + ' grid-column: span 5;', T.abruf_entfaellt)); return; }\n            if (!r.ok) { gp.appendChild(el('div', ZELLE_WERT + ROT + ' grid-column: span 5; white-space: normal;', T.fehlgeschlagen + (r.fehler ? ' (' + r.fehler + ')' : ''))); return; }\n            gp.appendChild(el('div', ZELLE_WERT + (r.status === 304 ? GUT : ''), String(r.status)));\n            gp.appendChild(el('div', ZELLE_WERT + GRAU, r.bytes === null ? '-' : String(r.bytes)));\n            gp.appendChild(el('div', ZELLE_WERT + GRAU, ms(r.ttfb)));\n            gp.appendChild(el('div', ZELLE_WERT + GRAU, r.age === null ? '-' : String(r.age)));\n            gp.appendChild(el('div', ZELLE_WERT + (r.cacheStatus && \/^(hit|stale|revalidated|updating)\/i.test(r.cacheStatus) ? GUT : GRAU), r.cacheStatus || '-'));\n        });\n        huelle.appendChild(gp);\n        ziel.appendChild(huelle);<\/p>\n<p>        ziel.appendChild(el('div', UEBERSCHRIFT, T.h_befunde));\n        var irgendwas = false;\n        STUFEN.forEach(function (s) {\n            var gr = b.gruppen[s];\n            if (!gr.length) { return; }\n            irgendwas = true;\n            ziel.appendChild(el('div', 'font-weight: 700; font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.5px; margin: 14px 0 6px; color: ' + FARBEN[s] + ';', T['stufe_' + s] + ' (' + gr.length + ')'));\n            gr.forEach(function (f) {\n                var k = el('div', KASTEN + ' border-left: 3px solid ' + FARBEN[s] + ';');\n                var t = text('befund_' + f.key, f.daten) || f.key;\n                var teile = t.split(' || ');\n                k.appendChild(el('div', 'color: var(--text-primary, #e8e8ee); font-size: 0.9rem; font-weight: 700;', teile[0]));\n                if (teile[1]) { k.appendChild(el('div', ZELLE + ' padding: 6px 0 0; line-height: 1.55;', teile[1])); }\n                ziel.appendChild(k);\n            });\n        });\n        if (!irgendwas) { ziel.appendChild(el('p', ZELLE + ' margin: 0;', T.keine_befunde)); }\n    }\n    window.LW_TEST.CC.darstellen = darstellen;<\/p>\n<p>    var laeuft = false;\n    btn.addEventListener('click', function () {\n        if (laeuft) { return; }\n        laeuft = true; btn.disabled = true;\n        out.innerHTML = '';\n        out.appendChild(el('p', ZELLE + GRAU + ' margin: 0;', T.laeuft));\n        abfragen(wert('cc-url')).then(function (a) { darstellen(bewerten(a), out); laeuft = false; btn.disabled = false; },\n            function () { darstellen(bewerten({ fehler: 'netz' }), out); laeuft = false; btn.disabled = false; });\n    });\n})();\n<\/script><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Reads the caching headers of an address, works out how long a browser and a shared cache may keep the response and on what basis, sends conditional requests to see whether ETag and Last-Modified really produce a 304, and checks whether a CDN or page cache in front is hitting.<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":38,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"template-tool-base.php","meta":{"footnotes":""},"tags":[91144,91243,91315],"class_list":["post-15552","page","type-page","status-publish","hentry","tag-devops","tag-networking","tag-web-performance"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Cache-Control &amp; Revalidation Analyzer - Lukas Wojcik - Blog<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/cache-control-revalidation-analyzer\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Cache-Control &amp; Revalidation Analyzer - Lukas Wojcik - Blog\" \/>\n<meta property=\"og:description\" content=\"Reads the caching headers of an address, works out how long a browser and a shared cache may keep the response and on what basis, sends conditional requests to see whether ETag and Last-Modified really produce a 304, and checks whether a CDN or page cache in front is hitting.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/cache-control-revalidation-analyzer\/\" \/>\n<meta property=\"og:site_name\" content=\"Lukas Wojcik - Blog\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/08\/og-default.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"630\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"1 minute\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/cache-control-revalidation-analyzer\\\/\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/cache-control-revalidation-analyzer\\\/\",\"name\":\"Cache-Control & Revalidation Analyzer - Lukas Wojcik - Blog\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\"},\"datePublished\":\"2026-09-05T11:43:40+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/cache-control-revalidation-analyzer\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/cache-control-revalidation-analyzer\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/cache-control-revalidation-analyzer\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Toolbox\",\"item\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"Cache-Control &#038; Revalidation Analyzer\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\",\"name\":\"Lukas Wojcik - Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\",\"name\":\"luky\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"contentUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"width\":424,\"height\":636,\"caption\":\"luky\"},\"logo\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\"},\"sameAs\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Cache-Control & Revalidation Analyzer - Lukas Wojcik - Blog","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/cache-control-revalidation-analyzer\/","og_locale":"en_US","og_type":"article","og_title":"Cache-Control & Revalidation Analyzer - Lukas Wojcik - Blog","og_description":"Reads the caching headers of an address, works out how long a browser and a shared cache may keep the response and on what basis, sends conditional requests to see whether ETag and Last-Modified really produce a 304, and checks whether a CDN or page cache in front is hitting.","og_url":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/cache-control-revalidation-analyzer\/","og_site_name":"Lukas Wojcik - Blog","og_image":[{"width":1200,"height":630,"url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/08\/og-default.jpg","type":"image\/jpeg"}],"twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"1 minute"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/cache-control-revalidation-analyzer\/","url":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/cache-control-revalidation-analyzer\/","name":"Cache-Control & Revalidation Analyzer - Lukas Wojcik - Blog","isPartOf":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#website"},"datePublished":"2026-09-05T11:43:40+00:00","breadcrumb":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/cache-control-revalidation-analyzer\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/cache-control-revalidation-analyzer\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/cache-control-revalidation-analyzer\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.lukaswojcik.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Toolbox","item":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/"},{"@type":"ListItem","position":3,"name":"Cache-Control &#038; Revalidation Analyzer"}]},{"@type":"WebSite","@id":"https:\/\/www.lukaswojcik.com\/blog\/#website","url":"https:\/\/www.lukaswojcik.com\/blog\/","name":"Lukas Wojcik - Blog","description":"","publisher":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.lukaswojcik.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9","name":"luky","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","contentUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","width":424,"height":636,"caption":"luky"},"logo":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg"},"sameAs":["https:\/\/www.lukaswojcik.com\/blog"]}]}},"_links":{"self":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/15552","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/comments?post=15552"}],"version-history":[{"count":0,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/15552\/revisions"}],"up":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/38"}],"wp:attachment":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/media?parent=15552"}],"wp:term":[{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/tags?post=15552"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}