Skip to main content

Caching, SSG, and ISR

Function cache​

import { cached, revalidatePath, revalidateTag } from '@nessframework/cache';

export const getPosts = cached(() => db.post.findMany(), {
key: 'posts',
life: 'minutes',
tags: ['posts'],
path: '/posts',
});

await revalidateTag('posts');
await revalidatePath('/posts');

Profiles include seconds, minutes, hours, days, max, and default. Concurrent calls are deduplicated. Expired values are regenerated; stale values are served while one background refresh runs.

Adapters​

MemoryCacheAdapter is the default and is process-local: a second instance keeps its own copy, and revalidateTag on one instance does not reach the others. Anything running more than one process needs a shared adapter.

Configure one in the server section of ness.config.mjs:

import { defineNessConfig } from '@nessframework/router';

export default defineNessConfig({
server: {
cache: { adapter: 'filesystem', directory: '.ness/cache' },
},
});
AdapterShared acrossNeedsUse it when
memorynothingβ€”development, single process
filesystemprocesses on one host, restartsa writable directoryone container, clustered Node, no external service
sqliteprocesses on one host, restartsNode.js 22.5+ (node:sqlite)same as above, with indexed invalidation
redisevery instancea Redis client you supplymore than one host or replica

Redis takes the client from your config rather than bundling one, so connection, TLS, and pooling stay yours:

import { createClient } from 'redis';

const client = await createClient({ url: process.env.REDIS_URL }).connect();

export default defineNessConfig({
server: {
cache: { adapter: 'redis', client, prefix: 'app:cache:' },
},
});

Tag and path invalidation​

Adapters expose keysByTag and keysByPath, so revalidateTag('posts') resolves the affected keys from an index instead of reading every cached entry. An adapter of your own may omit them β€” the cache falls back to a scan, which stays correct but costs one read per entry.

Local tier​

A shared store turns every cache hit into a network round trip. Set local to keep an in-process tier in front of it:

cache: { adapter: 'redis', client, local: true, bus }

The local tier reintroduces the problem the shared store solved: deleting an entry in Redis does not evict the copy another instance already holds in memory. Pass a bus so instances broadcast evictions to each other:

import { createRedisInvalidationBus } from '@nessframework/cache/tiered';

const bus = createRedisInvalidationBus(client, subscriber);

subscriber must be a separate connection β€” Redis does not allow other commands on a subscribed one. Without a bus, localTtl (5 seconds by default) bounds how long an instance may trust a local copy.

Static generation​

import { defineNessConfig } from '@nessframework/router';

export default defineNessConfig({
router: { prerender: ['/', '/pricing'] },
});

Prerendered HTML and data are emitted into build/client. Other pages use SSR. The Ness production server adds CDN-compatible s-maxage and stale-while-revalidate headers and performs incremental regeneration for anonymous HTML GET requests.

Requests with cookies or authorization headers bypass the default page cache.