docs/cookbook/http-caching-and-xsrf-cookie-refresh.md
By default, Inertia Rails rewrites the XSRF-TOKEN cookie on every protected request. Each rewrite emits a Set-Cookie header — including on otherwise cacheable GET responses and even on 304 Not Modified responses. If your app relies on HTTP caching for Inertia pages, that header can quietly defeat it. This page explains why it happens and how to opt into a less aggressive refresh policy. For the broader caching picture, see the caching guide.
The problem shows up in two places:
Set-Cookie. For example, Cloudflare either strips the header or sets the cache status to BYPASS, depending on cache settings. rack-cache, Varnish, and Fastly behave similarly by default. If every response carries a fresh XSRF-TOKEN, your cacheable pages never get cached.fresh_when / stale? expect reloads to settle into a clean If-None-Match / 304 loop. Repeated cookie writes on cacheable responses have been observed to prevent that, most notably in Safari/WebKit (Chrome and Firefox usually revalidate cookie-bearing responses correctly).The adapter refreshes the XSRF-TOKEN cookie in an after_action on every request where forgery protection is active, so Inertia's HTTP client can read it and send it back as an X-XSRF-TOKEN header.
Rails normally suppresses Set-Cookie when a cookie is written with its existing value — but that never applies here. Rails masks CSRF tokens with a random one-time pad on every call (a BREACH mitigation), so form_authenticity_token returns a different string on every request even though the underlying token hasn't changed. The cookie jar sees a new value every time and emits Set-Cookie on every response, including 304s.
Because the value is re-masked on every call, the only way to know whether the cookie actually needs a refresh is to unmask and compare it against the session token — which is exactly what the :lazy policy does when it can.
:lazy policyTo skip unnecessary rewrites on steady-state safe requests, opt into the :lazy policy:
InertiaRails.configure do |config|
config.xsrf_cookie_refresh = :lazy
end
You can also enable it for specific controllers only:
class CachedPagesController < ApplicationController
inertia_config xsrf_cookie_refresh: :lazy
end
Under :lazy, the adapter behaves as follows:
GET / HEAD requests with no XSRF-TOKEN cookie: set the cookie, as before.GET / HEAD requests with an existing cookie, when the session or CSRF token is already loaded: validate the cookie against the session and refresh it only if it's stale.GET / HEAD requests with an existing cookie, when nothing has loaded the session: trust the cookie and skip the write (see the caveat below).POST, PATCH, and so on): always refresh, as before.Conditionally cached pages take the validation path, not the trust path: fresh_when and stale? load the session (flash is part of the default ETag), so a stale cookie is detected and refreshed even on a 304 response.
This option matters most when all of the following are true:
fresh_when / stale? on Inertia pages, or serves them through a CDNGET304s or CDN cache hitsIt usually does not matter for apps that rely only on Inertia prefetching or server-side caching.
This option only addresses XSRF-TOKEN churn.
Cookie-backed sessions are a separate source of Set-Cookie churn: the session cookie is re-encrypted on every response that touches the session, and fresh_when / stale? touch the session. If your app stores sessions in cookies, :lazy alone won't produce fully Set-Cookie-free 304s — moving session storage to Redis or a database removes that second source.
The adapter also deliberately does not load a lazy session just to validate an existing cookie: loading a cookie-backed session emits its own session Set-Cookie, which would reintroduce the exact churn this option removes.
On GET / HEAD requests where nothing loads the session — no Inertia render, no flash, no fresh_when — an existing cookie is trusted without validation. A stale cookie (for example, after a secret_key_base rotation) is not detected on those requests, so the next protected request fails with InvalidAuthenticityToken.
This failure is loud, never a silent CSRF bypass: the cookie is not a server-side validation input (the server checks the X-XSRF-TOKEN header against the session). And it self-heals — any request that loads the session, including every rendered Inertia response, validates and refreshes the cookie.
The tradeoff is compatibility, not reduced CSRF protection:
InvalidAuthenticityToken, not as a silent bypass.If you need every safe request to refresh the XSRF cookie from the current session state, keep the default :always policy.
[!NOTE] Rails 8.2 introduces
protect_from_forgery using: :header_only, which verifies requests with theSec-Fetch-Siteheader instead of tokens. On apps using that strategy theXSRF-TOKENcookie serves no purpose at all, and a policy to skip it entirely may be added in a future release.:lazytargets today's token-based apps, including Rails 8.2's:header_or_legacy_tokenupgrade path.