Custom cache handler for Next.js with support for Google Cloud Storage and file-based caching. Designed for Pantheon's Next.js hosting platform.
- Dual Cache Handlers: Support for both GCS (production) and file-based (development) caching
- Next.js 16
use cacheSupport: Handlers for the newcacheHandlers(plural) API - Tag-Based Invalidation: Efficient O(1) cache invalidation using tag mapping
- Edge Cache Clearing: Automatic CDN cache invalidation on Pantheon infrastructure
- Build-Aware Caching: Automatically invalidates route cache on new builds
- Static Route Preservation: Preserves SSG routes during cache clearing
- Edge-Runtime Safe: Ships an edge-safe entry (resolved via the
edge-light/workerexport conditions) so a globally-configured cache handler doesn't break edge routes or edge middleware
npm install @pantheon-systems/nextjs-cache-handler// cacheHandler.ts
import { createCacheHandler } from '@pantheon-systems/nextjs-cache-handler';
const CacheHandler = createCacheHandler({
type: 'auto', // Auto-detect: GCS if CACHE_BUCKET exists, else file-based
});
export default CacheHandler;// next.config.mjs
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const nextConfig = {
cacheHandler: path.resolve(__dirname, "./cacheHandler.mjs"),
cacheMaxMemorySize: 0, // Disable in-memory caching to use custom handler
};
export default nextConfig;The GCS and file handlers are Node-only — they import fs and
@google-cloud/storage. When a cache handler is configured globally (via
cacheHandler / cacheHandlers in next.config, as Pantheon's build adapter
does zero-touch), Next.js bundles it into edge routes and edge middleware
too. If the entry point pulled in fs, those edge builds would fail with
edge runtime does not support Node.js 'fs' (or Can't resolve 'net').
To avoid that, the package ships a separate edge-safe entry exposing the same
API backed by no-op handlers, and maps it through the edge-light, worker,
workerd, and browser export conditions.
Next's edge compiler resolves the edge-safe entry; the Node server resolves the
real handlers. No configuration is needed — importing @pantheon-systems/nextjs-cache-handler
just works in both runtimes.
⚠️ Do not add this package totranspilePackages. Transpiling a package makes Next's edge compiler bundle its source and ignore theedge-lightexport condition, which drags the Node handlers back into the edge bundle and reintroduces thefs/netbuild failure. Leave it as a normal (externalized) dependency.
Edge routes/middleware never persist to the shared cache, so the edge no-op handlers are correct: reads miss and writes are dropped at the edge, while the Node server performs real caching.
Creates a cache handler based on the provided configuration.
interface CacheHandlerConfig {
/**
* Handler type selection:
* - 'auto': Automatically detect based on environment (GCS if CACHE_BUCKET is set, otherwise file)
* - 'file': Use file-based caching (local development)
* - 'gcs': Use Google Cloud Storage (production/Pantheon)
*/
type?: 'auto' | 'file' | 'gcs';
}Note: Debug logging is controlled via the
CACHE_DEBUGenvironment variable. See the Debugging section for details.
| Variable | Description | Required |
|---|---|---|
CACHE_BUCKET |
GCS bucket name for storing cache | Required for GCS handler |
OUTBOUND_PROXY_ENDPOINT |
Edge cache proxy endpoint (Pantheon infrastructure) | Optional (enables edge cache clearing) |
CACHE_DEBUG |
Enable debug logging (true or 1) |
Optional |
Factory function that returns the appropriate cache handler class based on configuration.
import { createCacheHandler } from '@pantheon-systems/nextjs-cache-handler';
// Auto-detect based on environment
const CacheHandler = createCacheHandler();
// Force file-based caching
const FileCacheHandler = createCacheHandler({ type: 'file' });
// Force GCS caching
const GcsCacheHandler = createCacheHandler({ type: 'gcs' });Returns cache statistics for the current environment.
import { getSharedCacheStats } from '@pantheon-systems/nextjs-cache-handler';
const stats = await getSharedCacheStats();
console.log(stats);
// {
// size: 10,
// keys: ['fetch:abc123', 'route:_index'],
// entries: [
// { key: 'fetch:abc123', tags: ['posts'], type: 'fetch', lastModified: 1234567890 }
// ]
// }Clears all cache entries (preserving static SSG routes).
import { clearSharedCache } from '@pantheon-systems/nextjs-cache-handler';
const clearedCount = await clearSharedCache();
console.log(`Cleared ${clearedCount} cache entries`);For advanced use cases, you can import the handlers directly:
import { FileCacheHandler, GcsCacheHandler } from '@pantheon-systems/nextjs-cache-handler';
// Use directly in your configuration
export default FileCacheHandler;Next.js 16 introduces the 'use cache' directive with a new cacheHandlers (plural) configuration. This package provides handlers for it.
createUseCacheHandler returns the handler class. Next.js's cacheHandlers
(plural) API expects each entry's default export to be an instance with
callable .get/.set/... methods — Next does not call new on it. So you must
instantiate the class with new and export the instance:
// use-cache-handler.mjs
import { createUseCacheHandler } from '@pantheon-systems/nextjs-cache-handler/use-cache';
const UseCacheHandler = createUseCacheHandler({
type: 'auto', // Auto-detect: GCS if CACHE_BUCKET exists, else file-based
});
// Note the `new` — Next.js calls methods directly on this exported value
// and will not instantiate the class for you.
export default new UseCacheHandler();If you forget the
new, Next.js builds will hang (~60s) and then fail because.get/.set/...are undefined on the class itself.
// next.config.mjs
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const nextConfig = {
// Existing handler for ISR, routes, fetch cache
cacheHandler: path.resolve(__dirname, "./cache-handler.mjs"),
// Handler for 'use cache' directive
cacheHandlers: {
default: path.resolve(__dirname, "./use-cache-handler.mjs"),
},
cacheMaxMemorySize: 0,
cacheComponents: true,
};
export default nextConfig;Factory function that returns the appropriate use-cache handler. Accepts the same type option ('auto', 'file', 'gcs').
Returns statistics for use cache entries, similar to getSharedCacheStats().
import { getUseCacheStats } from '@pantheon-systems/nextjs-cache-handler';
const stats = await getUseCacheStats();import { UseCacheFileHandler, UseCacheGcsHandler } from '@pantheon-systems/nextjs-cache-handler';The handler distinguishes between two cache types:
- Fetch Cache: Stores data from
fetch()calls with caching enabled - Route Cache: Stores rendered pages and route data
The handler maintains a tag-to-keys mapping for efficient O(1) cache invalidation:
// When setting cache with tags
await cacheHandler.set('post-1', data, { tags: ['posts', 'blog'] });
// When invalidating by tag
await cacheHandler.revalidateTag('posts');
// All entries tagged with 'posts' are invalidatedWhen deployed on Pantheon, the cache handlers automatically clear the CDN edge cache when cache entries are invalidated. This is triggered by:
revalidateTag()calls (clears matching surrogate keys and paths)revalidatePath()calls (clears the specific path from the CDN)
Edge cache clearing is enabled when the OUTBOUND_PROXY_ENDPOINT environment variable is set (automatically configured on Pantheon). It runs in the background and does not block cache operations.
On each new build, the handler automatically:
- Detects the new build ID
- Invalidates the route cache (Full Route Cache)
- Preserves the data cache (Fetch Cache)
This matches Next.js's expected behavior where route cache is invalidated on each deploy but data cache persists.
Enable debug logging to see detailed cache operations by setting the CACHE_DEBUG environment variable:
# Enable debug logging
CACHE_DEBUG=true npm run start
# Or
CACHE_DEBUG=1 npm run startThe cache handler uses four log levels:
| Level | When Shown | Use Case |
|---|---|---|
debug |
Only when CACHE_DEBUG=true |
Verbose operational logs (GET, SET, HIT, MISS) |
info |
Only when CACHE_DEBUG=true |
Important events (initialization, cache cleared) |
warn |
Always | Recoverable issues that might need attention |
error |
Always | Errors that affect cache operations |
When debug logging is enabled, you'll see output like:
[GcsCacheHandler] Initializing cache handler
[GcsCacheHandler] GET: /api/posts
[GcsCacheHandler] HIT: /api/posts (route)
[GcsCacheHandler] SET: /api/users (fetch)
[EdgeCacheClear] Cleared 3 paths in 45ms
[GcsCacheHandler] Revalidated 5 entries for tags: posts, blog
This helps diagnose cache behavior, verify cache hits/misses, and troubleshoot invalidation issues.
-
Ensure you're logged into npm with access to the
@pantheon-systemsscope:npm login --scope=@pantheon-systems
-
Verify your login:
npm whoami
-
Update the version in
package.json:# Patch release (0.1.0 -> 0.1.1) npm version patch # Minor release (0.1.0 -> 0.2.0) npm version minor # Major release (0.1.0 -> 1.0.0) npm version major
-
Build and test:
npm run build npm test -
Publish to npm:
npm publish --access public
The
--access publicflag is required for scoped packages to be publicly accessible.
After publishing, verify the package is available:
npm view @pantheon-systems/nextjs-cache-handlerOr install it in a test project:
npm install @pantheon-systems/nextjs-cache-handlerMIT