Stale while revalidate
Render a cached fragment immediately, then quietly refresh it when it becomes stale.
A tiny htmx extension for stale-while-revalidate caching, retries, request deduplication, and optimistic updates—without a client-side data store.
<!-- one attribute, instant cache --> <ul hx-get="/todos" hx-trigger="load" hx-swr="30"> </ul> // Cached for 30s. Stale content appears // immediately while htmx refreshes it.
Keep server-rendered HTML and htmx’s request model. Add the response-handling conveniences that make interfaces feel immediate and resilient.
Render a cached fragment immediately, then quietly refresh it when it becomes stale.
Retry GETs with exponential backoff. Unsafe verbs require an explicit opt-in.
Collapse identical in-flight GETs; each target renders when the winner lands.
Append a developer-authored pending template and remove only its nodes on failure.
Cached swaps preserve hx-swap and hx-select behavior.
No JSON cache, client store, offline queue, or SPA rewrite. Just rendered HTML.
>=2.0.0 <3 || >=4.0.0-beta6 <5.window.htmx exists.hx-ext="query".<script src="https://unpkg.com/htmx.org@4.0.0-beta6"></script> <script src="https://unpkg.com/htmx-query@0.2.0"></script> <body> ... </body>
htmx 2: use htmx.org@2.0.10 and add hx-ext="query". Production: pin exact CDN versions and add Subresource Integrity hashes. Floating versions cannot be integrity-checked.
Use the ESM build when htmx is supplied by your bundler.
import htmx from 'htmx.org'; import { register } from 'htmx-query'; register(htmx);
A fresh hit cancels the network request. A stale hit renders now and refreshes in the background.
<ul hx-get="/todos" hx-trigger="load, refresh" hx-swr="30"></ul> <!-- later: htmx.trigger(list, 'refresh') -->
Retries use exponential backoff. POST requests are never retried unless you explicitly opt in.
<button hx-get="/reports/today" hx-target="#report" hx-retry="3" hx-retry-delay="500"> Refresh report </button>
The template is removed before a real response swap and on request failure.
<form hx-post="/todos" hx-target="#list" hx-swap="beforeend" hx-optimistic="#pending">...</form> <template id="pending"><li>Saving…</li></template>
| Attribute | Use |
|---|---|
| hx-swr="TTL" | Cache a GET response; fresh entries cancel the request, stale entries revalidate. |
| hx-swr-key="key" | Override the default verb:final URL cache key. |
| hx-swr-vary="Header" | Opt in a safe request header such as Accept-Language as a key dimension. |
| hx-swr-prefetch="hover focus visible" | One same-origin GET on hover, keyboard focus, and/or viewport entry that fills the cache without swapping the source. |
| hx-retry="N" | Retry an eligible failed request up to N times (maximum 10). |
| hx-retry-delay="ms" | Set the retry backoff base; defaults to 1000 ms. |
| hx-retry-unsafe | Allow retries for non-GET requests. |
| hx-optimistic="#template" | Append pending template nodes until the request resolves. |
htmx.query.invalidate('/todos'); // remove matching cached entries htmx.query.clear(); // clear every entry htmx.query.peek(); // Map copy, for debugging htmx.query.stats(); // cache/dedupe counters and memory htmx.query.setNamespace('acme'); // clear old account data, then scope future keys document.body.addEventListener('hq:cache', observe); // hit/miss/store/evict/skip/clear
HTTP and account safety: cached HTML is server-rendered HTML and is reinserted verbatim. Age and Date reduce freshness; Expires provides a TTL when max-age is absent. no-store, private, and unkeyed request-header variation are never retained. An ETag entry revalidates with If-None-Match. Send HX-Cache-Invalidate: {"path":"/todos","mode":"path"} after a mutation to clear matching entries. Call setNamespace(accountId) after account changes.
llms.txt describes the API, invariants, source layout, test commands, and safe modification rules in a compact form. It is the fastest way for an agent to understand this repository before changing it.