Personalizing Sanity content without turning every GROQ query per-user and losing the CDN
Prototyped personalization by injecting user attributes into our GROQ queries. Result: Sanity CDN hit rate went from 96% to under 20% in two days. Obvious in hindsight, every request became unique so nothing is shared across visitors.
Rolling that back. But the requirement stands: different hero content per audience on a travel site where content queries must stay cacheable.
How are others structuring this? Keeping the queries generic while still making a per-user decision somewhere.
1 answer
The 96% to 20% drop is the expected outcome of that prototype: per-user parameters in GROQ queries defeat Sanity's CDN caching because the queries stop being shared across visitors.
The structure that preserves the hit rate is a split between content delivery and decisioning:
- Sanity queries stay generic and cacheable. Nothing user-specific enters GROQ, so the CDN keeps serving shared responses.
- The per-user decision happens in Croct, which resolves the variant for the slot server-side. Only the personalized component involves a per-request call; the rest of the page's content path is untouched.
On the latency cost of that one call: Croct's end-to-end response times are under 90 milliseconds at P95. And if a fetch ever fails, the Sanity content remains the default fallback, so the page degrades to the generic version rather than breaking.
The Sanity A/B testing and personalization guide walks through the component-to-slot mapping this split relies on.