DEV Community

Muhammad Usama
Muhammad Usama

Posted on

Nuxt SWR caching can leak one visitor's data to another — 3 traps I reproduced (and the fixes)

Turning on caching in Nuxt is one line:

routeRules: { '/products/**': { swr: 60 } }
Enter fullscreen mode Exit fullscreen mode

TTFB drops from seconds to milliseconds. But a cached response is built for one request and then handed to everyone whose request maps to the same cache key. On a personalized site, that's how one visitor's data ends up in another visitor's browser.

I hit these while caching a large e-commerce storefront, then rebuilt each one in a minimal Nuxt 4.5.2 / Nitro 2.13.4 app to make sure they're real. Here are three you can test today.

1. A cookie set during rendering is replayed to everyone

A page that assigns an A/B bucket during SSR:

<script setup lang="ts">
const bucket = useCookie('bucket')
if (!bucket.value) bucket.value = 'b-' + Math.random().toString(36).slice(2, 8)
</script>
Enter fullscreen mode Exit fullscreen mode

With swr: 60 on that route, three brand-new visitors got:

visitor 1 → set-cookie: bucket=b-ua93t6
visitor 2 → set-cookie: bucket=b-ua93t6
visitor 3 → set-cookie: bucket=b-ua93t6
Enter fullscreen mode Exit fullscreen mode

The Set-Cookie header is stored with the cached response. Your whole experiment collapses into one bucket (or worse, if it's a session-like cookie).

The contrast that points to the fix: the same kind of cookie set in server middleware was not cached — each visitor got their own value, because middleware runs on every request before the cache. So: set per-visitor cookies in server/middleware, not in pages, components or Nuxt plugins, and only when the value actually changes.

2. Multi-domain sites share one cache entry

One app, several hosts (stores, tenants, de. / fr. domains). The page prints the host:

Host: store-a.test → host=localhost
Host: store-b.test → host=localhost   ← whoever rendered first
Enter fullscreen mode Exit fullscreen mode

The default key is path-based. The fix, verified in the same lab:

routeRules: {
  '/': { swr: 60, cache: { varies: ['host', 'x-forwarded-host'] } },
}
Enter fullscreen mode Exit fullscreen mode

Now each host gets its own entry, and repeat requests are still served from cache.

3. Per-visitor content is frozen into the HTML

The page reads a currency cookie during SSR:

Cookie: currency=EUR → currency=USD
Cookie: currency=PKR → currency=USD   ← cached from the first, cookieless render
Enter fullscreen mode Exit fullscreen mode

Fixes, in order: keep personalized UI out of SSR (<ClientOnly> / client fetch), render a neutral value and verify on the client, vary the key only for a small bounded set (never by a per-visitor cookie), or don't cache the route. And if prices also depend on experiments or segments, a currency check alone won't save you.

A 60-second test for your own site

Request a cached route twice as two new visitors and compare cookies one by one, not as a block — a fresh middleware cookie can hide a replayed one next to it:

curl -s -D - -o /dev/null https://staging.example.com/route | grep -i set-cookie
curl -s -D - -o /dev/null https://staging.example.com/route | grep -i set-cookie
# Any cookie with the SAME value for both requests is coming from the cache.
Enter fullscreen mode Exit fullscreen mode

I wrote up all seven traps I've found (including ERR_HTTP_HEADERS_SENT during revalidation, state leaking across client navigation, per-process cache storage and measuring CLS the wrong way), with the lab app, a leak-test script and a Claude Code /cache-audit command that scans a Nuxt repo for these: Nuxt SSR Caching Without Leaks — code DEVTO takes 25% off.

Have you hit a caching leak I didn't list? I'd like to hear it in the comments.

Top comments (1)

Collapse
 
omyvnss profile image
Om Yaduvanshi •

the currency one is the commerce killer. a wrong price is not a cosmetic bug, it is a number the visitor cannot act on. the varies fix cuts both ways though: vary on currency everywhere and your hit rate quietly dies. a placeholder from the server with the real price resolved client-side is the safer default, and reserve key-varying for routes where currency actually changes the layout.