How to Fix: ISR – Nextjs 14.2.17 not saving Static Content on Redis on build time only using cache-handler

7 min read

Next.js 14.2.17 ISR pages are not being written to Redis during build because cache-handler does not automatically persist the initial static prerender output generated by next build. In this setup, Redis is typically used when the runtime cache layer is active during requests or revalidation, not as a guaranteed sink for every artifact emitted at build time.

Understanding the Root Cause

The confusion usually comes from expecting ISR, Redis, and cache-handler to behave like a unified storage pipeline during the build step. In Next.js 14.2.17, that is not how the system works.

There are two separate phases involved:

  1. Build-time prerendering: when next build generates static HTML and RSC payloads for routes that can be pre-rendered ahead of time.
  2. Runtime incremental caching: when a request hits the app and Next.js uses the incremental cache to store or update route output, fetch results, and revalidated content.

The key issue is that cache-handler integrates with the incremental cache lifecycle, not necessarily with every artifact written by the build pipeline. So if a page is fully prerendered during build, Next.js may place that output in its local build artifacts such as .next/server and prerender-manifest.json, while Redis remains empty until the page is actually requested or revalidated in a context where the custom cache handler is invoked.

That means the behavior is often:

  • Build succeeds.
  • Static ISR route exists in Next.js output.
  • No Redis entry appears immediately after build.
  • Redis starts receiving entries only after runtime access, regeneration, or cache population events.

This is why the issue looks like a bug, but in most cases it is a difference between build artifact storage and runtime cache storage.

Another technical detail: the Full Route Cache, Data Cache, and prerendered assets do not always map one-to-one into your custom Redis handler. Depending on how the app router route is rendered, whether it uses revalidate, whether it is dynamically rendered, and whether the page was visited after deploy, Redis may not be touched during build at all.

Step-by-Step Solution

If your goal is to make ISR content available in Redis immediately after deployment, the practical fix is to stop relying on build-time prerender output to populate Redis automatically. Instead, use one of these patterns:

  1. Accept that build output lives in Next.js artifacts and let Redis fill at runtime.
  2. Warm the cache after deploy by requesting target routes.
  3. Explicitly precompute and write needed data into Redis through your own script or API workflow.

The most reliable solution is post-deploy cache warming.

1. Configure ISR correctly on the route

Make sure the route is actually using ISR and not fully dynamic rendering.

export const revalidate = 3600

export default async function Page() {
  const data = await fetch('https://example.com/api/content', {
    next: { revalidate: 3600 }
  }).then((r) => r.json())

  return <main>{data.title}</main>
}

This ensures the route participates in incremental caching. If you accidentally force dynamic rendering, nothing useful will be persisted for ISR.

2. Verify your custom cache handler is loaded at runtime

Your Redis-backed handler must be used by the running server, not just present in the repository.

import { CacheHandler } from 'your-cache-handler-package'
import Redis from 'ioredis'

const redis = new Redis(process.env.REDIS_URL)

export default class RedisHandler extends CacheHandler {
  async get(key) {
    const value = await redis.get(key)
    return value ? JSON.parse(value) : null
  }

  async set(key, data) {
    await redis.set(key, JSON.stringify(data))
  }
}

Then confirm your Next.js configuration references it correctly.

const nextConfig = {
  cacheHandler: require.resolve('./RedisHandler.js')
}

module.exports = nextConfig

If the handler is misconfigured, Redis will never receive entries, whether during build or runtime.

3. Do not expect Redis keys immediately after next build

This is the core behavior to understand. After running:

next build

check the generated manifests and build output instead of Redis first. For example, verify that the route is actually prerendered:

cat .next/prerender-manifest.json

If the route is listed there, Next.js has built the static page correctly. The absence of Redis keys at this point does not mean ISR is broken.

4. Warm Redis after deployment

After starting the production server, request the ISR routes so the runtime cache layer executes and writes to Redis.

curl -I http://localhost:3000/your-isr-route
curl -I http://localhost:3000/another-route

For multiple routes, automate it with a script:

const routes = [
  '/',
  '/blog',
  '/products/item-1'
]

async function warm() {
  for (const route of routes) {
    const res = await fetch(`http://localhost:3000${route}`)
    console.log(route, res.status)
  }
}

warm().catch(console.error)

This approach is simple, deployment-safe, and aligned with how incremental cache population actually works.

5. If you need true build-time persistence in Redis, implement it yourself

If your requirement is strict—meaning the generated HTML must exist in Redis before the first user request—you need a custom export pipeline. One common pattern is:

  1. Run next build.
  2. Read known prerendered routes from prerender-manifest.json.
  3. Load generated HTML and related payload files from .next/server.
  4. Write those assets into Redis using your own key convention.

Example starter script:

const fs = require('fs')
const path = require('path')
const Redis = require('ioredis')

const redis = new Redis(process.env.REDIS_URL)
const manifestPath = path.join(process.cwd(), '.next', 'prerender-manifest.json')
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))

async function syncPrerenderedPages() {
  const routes = manifest.routes || {}

  for (const route of Object.keys(routes)) {
    let filePath = route === '/'
      ? path.join(process.cwd(), '.next', 'server', 'app', 'index.html')
      : path.join(process.cwd(), '.next', 'server', 'app', route, 'index.html')

    if (!fs.existsSync(filePath)) {
      console.log('Skipping missing file for route:', route)
      continue
    }

    const html = fs.readFileSync(filePath, 'utf8')
    await redis.set(`prerender:${route}`, html)
    console.log('Stored route in redis:', route)
  }
}

syncPrerenderedPages()
  .then(() => process.exit(0))
  .catch((err) => {
    console.error(err)
    process.exit(1)
  })

You will likely need to adjust paths for your routing mode, deployment target, and whether you are using the App Router or a different artifact layout. Still, this is the correct architectural fix if Redis must be prefilled before traffic arrives.

6. Validate in production mode, not development

ISR and cache behavior can differ significantly in dev. Always test with:

next build
next start

Then inspect Redis while requesting the route.

Common Edge Cases

  • Route is actually dynamic: using functions such as headers(), cookies(), or uncached fetch patterns can force dynamic rendering and bypass expected ISR persistence.
  • revalidate is missing or overridden: if no route-level or fetch-level revalidation is set, the cache path may differ from what you expect.
  • Only fetch cache is stored: sometimes developers expect full HTML in Redis, but only data cache entries are being persisted.
  • Wrong Redis key inspection: your handler may serialize keys with prefixes, hashes, or route metadata, so checking for a human-readable URL key can be misleading.
  • Serverless or container restarts: local filesystem artifacts may disappear between instances, making Redis warming more important.
  • App Router file layout differences: generated files are not always where older Pages Router examples suggest.
  • Handler not invoked during build: this is the exact issue here and should be treated as expected unless your implementation explicitly bridges build output into Redis.

FAQ

Why does the page work as ISR if Redis is empty right after build?

Because Next.js can serve the prerendered page from its build artifacts. Redis is not necessarily the source of truth for the initial build output.

Is this a bug in Next.js 14.2.17?

In most cases, no. It is usually a misunderstanding of how build-time prerendering differs from runtime incremental cache persistence. The custom cache-handler is not guaranteed to receive every prerendered page during build.

How do I force Redis to contain ISR pages before the first request?

Use a post-build sync script that reads Next.js prerender artifacts and writes them to Redis, or run a cache warming step immediately after deployment by requesting the routes.

The practical takeaway is simple: cache-handler in Next.js 14.2.17 is not a build artifact exporter. It is primarily a runtime cache integration point. Once you separate those responsibilities, the fix becomes straightforward: either warm the cache after deploy or explicitly copy prerendered output into Redis yourself.

Leave a Reply

Your email address will not be published. Required fields are marked *