How to Fix: [dynamicIO] cacheHandlers are not used in production
[dynamicIO] cacheHandlers are not used in production: why it happens and how to fix it
The bug shows up in the most confusing way possible: custom cache handlers work in development, then silently fall back to the default behavior in production. If you are testing a Next.js app that relies on dynamicIO and custom cacheHandlers, this usually means your production server is not actually loading the handler configuration at runtime the same way development does.
You can review the reproduction in the example repository.
Understanding the Root Cause
This issue happens because development mode and production mode do not initialize caching infrastructure in exactly the same way.
In development, Next.js tends to evaluate configuration more directly and with fewer production optimizations. That can make a custom cache handler appear to work correctly during local testing. In production, however, Next.js uses the built output and a stricter runtime path for initializing incremental cache behavior. If the custom handler is not bundled, resolved, or enabled exactly where production expects it, Next.js will fall back to its built-in cache mechanism.
With dynamicIO, this becomes easier to notice because cache reads and writes are part of the request lifecycle in a way that affects rendered output immediately. The result is a mismatch:
- Development: custom handler appears active.
- Production: custom handler is ignored or never instantiated.
Technically, the root causes usually fall into one or more of these categories:
- The cacheHandlers configuration is only honored in specific runtime paths.
- The handler file is not resolved correctly after next build.
- The production server is using a mode where dynamicIO support and custom caching integration are still incomplete or inconsistent.
- The app is deployed in an environment where the expected Node.js runtime behavior differs from local development.
In short, this is not typically a bug in your handler logic itself. It is usually a production initialization mismatch between how Next.js loads custom cache infrastructure in development versus in the compiled server output.
Step-by-Step Solution
The safest approach is to verify three things in order: your configuration, your runtime, and whether production actually instantiates the custom handler.
1. Confirm the custom handler is configured in next.config.js
Make sure your config explicitly enables the relevant experimental behavior and points to the correct handler module.
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
dynamicIO: true,
cacheHandlers: {
default: require.resolve('./cache-handler.js'),
},
},
}
module.exports = nextConfig
Key points:
- Use require.resolve() when possible so the module path is explicit.
- Keep the handler in a location that will still be valid after build.
- Do not rely on ambiguous relative paths.
2. Verify the handler exports the expected API
Your custom cache handler must match the interface expected by Next.js. A minimal example looks like this:
class CustomCacheHandler {
constructor(options) {
console.log('CustomCacheHandler initialized', options)
}
async get(key) {
console.log('get', key)
return null
}
async set(key, data, ctx) {
console.log('set', key, ctx)
}
async revalidateTag(tag) {
console.log('revalidateTag', tag)
}
async resetRequestCache() {
console.log('resetRequestCache')
}
}
module.exports = CustomCacheHandler
The console logging is important here. It gives you a direct signal about whether the handler is ever constructed in production.
3. Test development and production separately
Do not assume a successful dev test proves production support.
pnpm run dev
Then build and run production:
pnpm run build
pnpm run start
If you see handler logs in development but not in production, you have confirmed the issue is in production handler loading, not in your cache logic.
4. Inspect the built server output
After building, verify that the handler file is still reachable from the production server process. If your handler path references a source file that is not available in the final runtime layout, production cannot load it.
ls -R .next
Also inspect your project structure to ensure the referenced file exists where the production process expects it.
ls -R .
If the handler is outside the packaged runtime context, move it into the application root or another stable location and update the config path.
5. Force a simple reproduction signal
Add unmistakable side effects to the handler so you can detect usage immediately.
const fs = require('fs')
const path = require('path')
class CustomCacheHandler {
constructor() {
fs.appendFileSync(path.join(process.cwd(), 'cache-handler.log'), 'initialized\n')
}
async get(key) {
fs.appendFileSync(path.join(process.cwd(), 'cache-handler.log'), `get:${key}\n`)
return null
}
async set(key) {
fs.appendFileSync(path.join(process.cwd(), 'cache-handler.log'), `set:${key}\n`)
}
async revalidateTag(tag) {
fs.appendFileSync(path.join(process.cwd(), 'cache-handler.log'), `tag:${tag}\n`)
}
async resetRequestCache() {}
}
module.exports = CustomCacheHandler
Now compare dev and prod behavior by checking whether the log file is written.
6. Use a stable Node.js server runtime
Custom cache handlers are intended for a Node.js runtime. If you are trying to validate this in an environment that behaves more like Edge runtime or a constrained serverless wrapper, production behavior may differ from local Node execution.
If needed, make the route explicitly run on Node:
export const runtime = 'nodejs'
7. Upgrade Next.js to the latest compatible version
Because dynamicIO and related cache internals are highly experimental, production inconsistencies may already be fixed in a newer release. Before implementing workarounds, upgrade and retest.
pnpm up next
Then rebuild:
pnpm run build
pnpm run start
8. Practical workaround if production support is still broken
If your investigation confirms that cacheHandlers are currently not being honored in production for this setup, the most reliable workaround is to avoid depending on that integration for critical production logic. Instead:
- Use built-in Next.js caching where possible.
- Move custom persistence into application-level data storage.
- Use explicit invalidation flows outside the experimental handler path.
For example, wrap your own cache calls in a utility instead of relying entirely on framework-level handler injection:
const cache = new Map()
export async function getCachedValue(key, factory) {
if (cache.has(key)) return cache.get(key)
const value = await factory()
cache.set(key, value)
return value
}
This is not a full replacement for framework caching, but it prevents production behavior from silently diverging while the underlying issue remains unresolved.
Common Edge Cases
- Wrong file path: the handler works in dev because the source tree is available, but production resolves files from a different location.
- Unsupported runtime: the route or deployment target is not using the expected Node.js environment.
- Experimental flag mismatch: dynamicIO or cacheHandlers is enabled locally but not in the actual production config.
- Multiple configs: a monorepo may contain more than one next.config.js, and the wrong one may be used during build.
- No observable signal: the handler may be failing silently, so without constructor logging you cannot tell whether it was loaded.
- Version-specific behavior: one Next.js release may partially support a feature that behaves differently in another patch release.
FAQ
Why do cacheHandlers work in development but not after next build?
Because development and production use different initialization flows. In production, the built server must resolve and instantiate the custom handler from compiled output. If that path is unsupported or incorrectly resolved, Next.js falls back to its default cache behavior.
Is this caused by a bug in my custom cache handler implementation?
Usually not. If your handler logs appear in development but never appear in production, the problem is more likely that production is not loading the handler at all rather than a bug inside get, set, or revalidateTag.
What is the safest production workaround right now?
The safest option is to avoid making business-critical production behavior depend on experimental cacheHandlers. Use built-in caching where possible, or move custom cache persistence into application code you fully control and can verify independently of Next.js internals.
The key takeaway is simple: this issue is primarily a production integration gap, not just a bad config line. Verify handler instantiation, confirm runtime compatibility, test with explicit logging, and treat current dynamicIO cacheHandlers behavior as experimental until production parity is proven in your exact Next.js version.