---
title: "Runtime Cache"
description: "How OG image caching works at runtime, including internal storage, CDN edge caching, and platform integration."
canonical_url: "https://nuxtseo.com/docs/og-image/guides/runtime-cache"
last_updated: "2026-05-06T18:43:38.586Z"
---

When generating images at runtime, Nuxt OG Image provides two layers of caching to minimise server load and response times. For most deployments, the defaults work well and no configuration is needed.

## Quick Start

The module caches by default with a 72-hour TTL. For production, the only recommended change is adding persistent storage so your cache survives server restarts:

<code-group>

```ts [Cloudflare KV]
export default defineNuxtConfig({
  ogImage: {
    runtimeCacheStorage: {
      driver: 'cloudflare-kv-binding',
      binding: 'OG_IMAGE_CACHE'
    }
  }
})
```

```ts [Redis]
export default defineNuxtConfig({
  ogImage: {
    runtimeCacheStorage: {
      driver: 'redis',
      host: 'localhost',
      port: 6379
    }
  }
})
```

```ts [NuxtHub]
// Auto-detected when @nuxthub/core is installed with KV enabled
export default defineNuxtConfig({
  modules: ['@nuxthub/core', 'nuxt-og-image'],
  hub: { kv: true }
})
```

</code-group>

That's it. The module handles cache headers, CDN integration, and cache key generation automatically.

## How It Works

### Layer 1: Internal Cache (Nitro Storage)

The module caches rendered image buffers in Nitro storage. On a cache hit, the image is served in ~5-30ms instead of re-rendering (400-3500ms).

By default, this uses **in-memory** storage. The cache is lost on server restart or redeployment. Configuring a [persistent storage driver](#persistent-storage) fixes this.

### Layer 2: CDN Edge Cache

Every response includes HTTP cache headers that instruct CDN proxies to cache at the edge:

```text
Cache-Control: public, max-age=259200, s-maxage=259200, immutable
ETag: W/"<hash>"
Last-Modified: <timestamp>
Vary: accept-encoding, host
```

<table>
<thead>
  <tr>
    <th>
      Directive
    </th>
    
    <th>
      Purpose
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        max-age
      </code>
    </td>
    
    <td>
      Browser cache duration
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        s-maxage
      </code>
    </td>
    
    <td>
      CDN/proxy cache duration
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        immutable
      </code>
    </td>
    
    <td>
      Skip revalidation; URLs are content-addressed (params + component hash), so the response bytes never change for a given URL
    </td>
  </tr>
</tbody>
</table>

When the CDN has a cached copy, requests never reach your server.

### Auto-Configured Cache Headers

The module sets `Cache-Control` on every OG image response so CDNs cache it correctly without manual route rules:

<table>
<thead>
  <tr>
    <th>
      Endpoint
    </th>
    
    <th>
      <code>
        Cache-Control
      </code>
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        /_og/s/**
      </code>
      
       (prerendered)
    </td>
    
    <td>
      <code>
        public, max-age=31536000, immutable
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        /_og/d/**
      </code>
      
       (dynamic)
    </td>
    
    <td>
      <code>
        public, max-age=<ttl>, s-maxage=<ttl>, immutable
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        /_og/r/**
      </code>
      
       (resolver)
    </td>
    
    <td>
      Same as dynamic
    </td>
  </tr>
</tbody>
</table>

Where `<ttl>` is `cacheMaxAgeSeconds` (default 72 hours). URLs are content-addressed (encoded params + component hash), so `immutable` is safe; the response bytes never change for a given URL. No Nitro `swr`/`isr` route rule is needed (and it wouldn't work anyway, as `cachedEventHandler` JSON-serializes responses and breaks binary PNG/JPEG output).

## Query Parameters

OG image URLs encode all options in the **URL path**, not query parameters:

```text
/_og/d/w_1200,h_600,c_NuxtSeo,title_Hello+World.png
```

The cache key is deterministic from the path alone. Appending query parameters like `?ref=twitter` or `?utm_source=og` directly to an OG image URL has no effect on rendering. If you need different image variants, use a [custom cache key](#custom-cache-key).

Note that your **page URL's** query parameters (e.g., `/products?page=2`) are encoded into the generated OG image URL via the `_query` field. This means different page query strings produce different OG image URLs, since the page may render differently with different query params. Each unique combination gets its own cached image.

## Persistent Storage

The default in-memory cache is lost on server restart. For production you have two options:

### Option 1: Configure Nitro's `cache` storage (recommended)

The module writes cache entries under Nitro's `cache:` namespace, so pointing Nitro at a persistent driver is enough; no module-specific config needed:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  nitro: {
    storage: {
      cache: {
        driver: 'redis',
        host: 'localhost',
        port: 6379,
      },
    },
  },
})
```

This also benefits anything else in your app that caches via Nitro (e.g. `cachedFunction`, `cachedEventHandler`, SWR route rules).

### Option 2: `runtimeCacheStorage`

If you want OG images on a separate mount from the rest of your Nitro cache, use `runtimeCacheStorage`, which accepts the same configuration as Nitro's [`storage`](https://nitro.build/guide/storage) option.

### Using Runtime Config

If you need environment-specific configuration (e.g., different [Redis](https://redis.io) hosts per environment), mount your own storage driver and reference it by key:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  ogImage: {
    runtimeCacheStorage: 'og-image-cache'
  }
})
```

Then mount the storage in a Nitro plugin:

```ts [server/plugins/og-image-cache.ts]
import redisDriver from 'unstorage/drivers/redis'

export default defineNitroPlugin(() => {
  const config = useRuntimeConfig()

  const driver = redisDriver({
    base: 'og-image',
    host: config.redis.host,
    port: config.redis.port,
    password: config.redis.password,
  })

  useStorage().mount('og-image-cache', driver)
})
```

## Cache Time

The default TTL is 72 hours (3 days). You can change it globally via `cacheMaxAgeSeconds` or per image.

### Global Default

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  ogImage: {
    cacheMaxAgeSeconds: 60 * 60 * 24 // 1 day
  }
})
```

This controls the internal storage TTL and HTTP `Cache-Control` header duration (`max-age` / `s-maxage`).

### Per Image

```ts
defineOgImage('NuxtSeo', { title: 'Hello' }, { cacheMaxAgeSeconds: 30 })
```

## Custom Cache Key

For full control over caching, provide a custom cache key:

```ts
defineOgImage('NuxtSeo', { title: 'Hello' }, { cacheKey: 'my-custom-key' })
```

This is useful when you need to cache based on criteria that aren't part of the URL.

## Cache Version

By default, the cache is namespaced with the module version. Upgrading `nuxt-og-image` automatically invalidates all cached images.

To persist cache across module upgrades, set a static version or disable versioning:

<code-group>

```ts [Static Version]
export default defineNuxtConfig({
  ogImage: {
    cacheVersion: 'v1',
  }
})
```

```ts [Disable]
export default defineNuxtConfig({
  ogImage: {
    cacheVersion: false
  }
})
```

</code-group>

## Purging the Cache

Append `?purge` to any OG image URL to invalidate its internal cache entry and force a fresh render.

When [URL signing](/docs/og-image/guides/security#url-signing) is enabled, you must provide the signing secret as the purge value: `?purge=<your-secret>`. This prevents unauthorized cache invalidation.

Note that this only clears the module's internal Nitro storage cache (Layer 1). CDN edge caches (Layer 2) may continue serving the previous version until their TTL expires. To force CDN re-fetch:

- **Vercel**: Redeploy, or use the [Vercel CLI](https://vercel.com/docs/cli) to purge
- **Cloudflare**: Purge via the dashboard or API
- **Social platforms**: Use our [Social Share Debugger](/tools/social-share-debugger) to force Twitter, Facebook, and LinkedIn to re-fetch

<callout icon="i-heroicons-arrow-path" to="/tools/social-share-debugger">

**Clear social platform caches**: Use our [Social Share Debugger](/tools/social-share-debugger) to force re-fetching.

</callout>

## Disabling the Cache

While not recommended, you can disable caching entirely:

<code-group>

```vue [Single Image]
<script lang="ts" setup>
defineOgImage('NuxtSeo', { title: 'Hello' }, {
  cacheMaxAgeSeconds: 0
})
</script>
```

```ts [Global]
export default defineNuxtConfig({
  ogImage: {
    runtimeCacheStorage: false,
  }
})
```

</code-group>
