Back to Blog
Tutorial10 min read2026-08-07

Session Caching in a Nuxt Static Site with the KV Store

Speed up a Nuxt SSG build with Redis caching — provision a KV store, cache API responses at build time, and avoid rate limits from external services.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Nuxt's static generation (nuxt generate) fetches data at build time, renders pages to HTML, and outputs a static site. If you're pulling from a headless CMS or a third-party API, every build hits those endpoints hundreds of times — once per page, once per component that calls useFetch. The API rate-limits you, or the build takes eight minutes because each request has 200ms latency.

The fix: cache responses in Redis. On the first build, fetch from the live API and store the result in the KV store. On subsequent builds, read from the cache. The API sees one request instead of a hundred, your build finishes in seconds, and you control cache invalidation instead of hoping the CMS doesn't change its rate limits.

Provisioning the KV store

The KV store is a managed Redis instance, separate from SQL databases. Provision it via the API:

curl -X POST https://api.pandastack.io/v1/kv \
  -H "Authorization: Bearer $PANDASTACK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "nuxt-cache"
  }'

Response: { "success": true, "data": { "kvId": "kv_abc123", "restApiUrl": "...", "restApiToken": "..." } }.

Save the kvId. You'll link it to the Nuxt project during deployment.

Linking the KV store to the Nuxt project

When you deploy the static site, pass the kvId:

curl -X POST https://api.pandastack.io/v1/projects \
  -H "Authorization: Bearer $PANDASTACK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "static",
    "name": "nuxt-blog",
    "repositoryName": "yourorg/nuxt-blog",
    "branch": "main",
    "autoDeploy": true,
    "buildCommand": "npm run generate",
    "outputDir": ".output/public",
    "kvId": "kv_abc123"
  }'

The build environment now includes:

  • KV_REST_API_URL — the HTTP endpoint for Redis
  • KV_REST_API_TOKEN — bearer token for authentication

Your Nuxt build scripts can read these via process.env and cache API responses.

Caching CMS data at build time

Nuxt 3 uses useFetch and useAsyncData for data fetching. These run at build time (SSG mode) and at request time (SSR mode). For static sites, everything runs at build time.

Here's a composable that checks the cache first, hits the live API on a miss, and stores the result:

// composables/useCachedFetch.ts
export async function useCachedFetch<T>(
  key: string,
  url: string,
  ttl: number = 3600
): Promise<T> {
  const restUrl = process.env.KV_REST_API_URL;
  const restToken = process.env.KV_REST_API_TOKEN;

  if (!restUrl || !restToken) {
    console.warn('KV store not configured, fetching directly');
    const response = await fetch(url);
    return response.json();
  }

  // Try cache first
  const cacheRes = await fetch(`${restUrl}/get/${key}`, {
    headers: { Authorization: `Bearer ${restToken}` }
  });

  if (cacheRes.ok) {
    const cached = await cacheRes.json();
    if (cached.result) {
      console.log(`Cache HIT: ${key}`);
      return JSON.parse(cached.result);
    }
  }

  // Cache miss — fetch from live API
  console.log(`Cache MISS: ${key}`);
  const response = await fetch(url);
  const data = await response.json();

  // Store in cache with TTL
  await fetch(`${restUrl}/setex/${key}/${ttl}`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${restToken}`,
      'Content-Type': 'text/plain'
    },
    body: JSON.stringify(data)
  });

  return data;
}

Use it in a page component:

<script setup lang="ts">
const posts = await useCachedFetch(
  'cms:posts',
  'https://cms.example.com/api/posts',
  1800 // 30 minutes TTL
);
</script>

<template>
  <div>
    <h1>Blog Posts</h1>
    <ul>
      <li v-for="post in posts" :key="post.id">
        {{ post.title }}
      </li>
    </ul>
  </div>
</template>

On the first build, the cache is empty. The composable hits the CMS, gets the posts, stores them in Redis, and renders the page. On the second build (within 30 minutes), the cache returns the stored data, and the CMS never sees a request.

Caching per-page data

If you have 100 blog posts and each has its own page, cache them individually:

<script setup lang="ts">
const route = useRoute();
const postId = route.params.id as string;

const post = await useCachedFetch(
  `cms:post:${postId}`,
  `https://cms.example.com/api/posts/${postId}`,
  1800
);
</script>

<template>
  <article>
    <h1>{{ post.title }}</h1>
    <div v-html="post.content" />
  </article>
</template>

On the first build, all 100 posts are fetched and cached. On subsequent builds, only posts that expired (or were never cached) hit the CMS. If you publish a new post, the cache misses for that ID, fetches it, and stores it. Old posts stay cached.

Invalidating the cache manually

If the CMS changes and you need to force a fresh fetch, delete the cache key:

curl -X DELETE https://<rest-api-url>/del/cms:posts \
  -H "Authorization: Bearer $KV_REST_API_TOKEN"

The next build hits the live API again.

Alternatively, set a short TTL (60 seconds) during development and a long TTL (3600 seconds) in production. The cache refreshes quickly when you're iterating, but stays stable in production.

Comparing the KV store to Nuxt's payload cache

Nuxt 3 has a built-in payload cache (stored in .nuxt/cache). It works locally but doesn't persist across deploys — each build starts with an empty cache. The KV store persists across builds, so the cache survives redeployments.

For CI/CD workflows where builds run in ephemeral containers, the KV store is the only option.

Using a native Redis client instead

If you prefer ioredis:

import Redis from 'ioredis';

const redis = new Redis(process.env.KV_URL!);

export async function useCachedFetch<T>(
  key: string,
  url: string,
  ttl: number = 3600
): Promise<T> {
  const cached = await redis.get(key);
  if (cached) {
    console.log(`Cache HIT: ${key}`);
    return JSON.parse(cached);
  }

  console.log(`Cache MISS: ${key}`);
  const response = await fetch(url);
  const data = await response.json();

  await redis.setex(key, ttl, JSON.stringify(data));
  return data;
}

The KV_URL environment variable includes TLS and credentials, so the client connects and authenticates automatically.

Important: redis-cli does not work directly because it doesn't send TLS SNI. Use the REST API with curl or a native client like ioredis.

Deploying with pandastack.json

For a repo others might deploy, commit the config:

{
  "type": "static",
  "name": "nuxt-blog",
  "buildCommand": "npm run generate",
  "outputDir": ".output/public"
}

Add a deploy button to the README:

[![Deploy to PandaStack](https://dashboard.pandastack.io/deploy-button.svg)](https://dashboard.pandastack.io/deploy?repo=yourorg/nuxt-blog)

The user clicks the button, the platform reads pandastack.json, and prompts for any missing environment variables (like KV_URL if the KV store isn't linked yet).

What happens if the cache is down

If the KV store is unreachable (network issue, Redis down), the composable falls back to fetching directly from the API:

if (!restUrl || !restToken) {
  console.warn('KV store not configured, fetching directly');
  const response = await fetch(url);
  return response.json();
}

The build doesn't fail — it just runs slower because every request hits the live API.

Using the CLI to deploy

panda login
panda projects create \
  --repo yourorg/nuxt-blog \
  --branch main \
  --name nuxt-blog \
  --type static \
  --build-command "npm run generate" \
  --output-dir ".output/public" \
  --kv-id kv_abc123

The CLI reads pandastack.json if it exists, or you can pass all parameters as flags.

What you've deployed

A Nuxt static site that caches CMS responses in Redis at build time, avoiding rate limits and speeding up builds. The KV store is provisioned independently, linked to the project via kvId, and accessed over HTTP using the REST API or a native Redis client.

The static site is served from a CDN (zero idle cost, instant cold starts). The KV store runs continuously and bills for allocated memory, not query volume. Free-tier apps include edge functions and the KV store with reasonable limits for development and hobby projects.

Redeployments purge the CDN cache automatically, so users never see stale HTML. The Redis cache TTL controls how often the Nuxt build fetches fresh data from upstream APIs — set it short (60s) for frequently changing content, long (3600s) for stable data.

References

  • [Nuxt data fetching](https://nuxt.com/docs/getting-started/data-fetching)
  • [Redis commands](https://redis.io/commands/)
  • [PandaStack KV store](https://docs.pandastack.io/kv/)
  • [PandaStack static sites](https://docs.pandastack.io/projects/static/)

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also