Redis pub/sub
A Dynamic Snapshot serves all your content from an in-memory snapshot on the server and hot-refreshes it when you publish, with no rebuild and no API call per read. The pattern has two pieces: where the snapshot is read from (snapshotLoader) and who triggers the refresh (refreshSnapshot()).
In this version the JSON lives in Redis. A publisher exports the snapshot with exportSnapshot(), does SET + PUBLISH, and all subscribed instances refresh at once with a single message.
How does it work?
- Source: Redis. The
snapshotLoaderreads the snapshot key. - Trigger with pub/sub: instead of one
POSTper instance, a singlePUBLISHnotifies all subscribed replicas and each one callsrefreshSnapshot().
When to choose it?
- Designed for horizontal scaling: refresh N instances with a single message, without per-instance webhooks or N API calls.
- Requires a Redis reachable by all replicas.
Step by step
Start from the endpoint version project. We swap the snapshotLoader for Redis, add the subscription and a publisher.
-
Start Redis and install the dependencies:
Terminal window docker run -d --name redis -p 6379:6379 redis:7npm i redisnpm i -D tsx dotenv # to run the TypeScript publisher outside Next.js.env REDIS_URL=redis://localhost:6379 -
Create the Redis connection (inside Next.js the
.envis loaded automatically):lib/redis.ts import { createClient as createRedis } from 'redis';export const redis = createRedis({ url: process.env.REDIS_URL });export const redisReady = redis.connect(); -
Change the
snapshotLoaderto read from Redis and subscribe to the channel. On first use, each instance listens and refreshes itself when the publisher notifies. If the snapshot doesn’t exist in Redis yet, we don’t cache the failure, so the next request retries:lib/content-island.ts import { createClient } from '@content-island/api-client';import { redis, redisReady } from './redis';const accessToken = process.env.CONTENT_ISLAND_TOKEN!;const SNAPSHOT_KEY = 'content-island:snapshot';const CHANNEL = 'content-island:updated';const globalForClient = globalThis as unknown as {contentIslandClient?: ReturnType<typeof createClient>;primed?: Promise<unknown> | null;};export const contentIslandClient =globalForClient.contentIslandClient ??createClient({accessToken,mode: 'snapshot',// The loader reads the snapshot from Redis.snapshotLoader: async () => {await redisReady;return (await redis.get(SNAPSHOT_KEY)) ?? '';},});globalForClient.contentIslandClient = contentIslandClient;// On first use, this instance subscribes to the channel and refreshes itself// every time the publisher notifies. It also does the initial snapshot load.export function ensureSnapshot() {if (!globalForClient.primed) {globalForClient.primed = (async () => {await redisReady;const sub = redis.duplicate();await sub.connect();await sub.subscribe(CHANNEL, () => {contentIslandClient.refreshSnapshot().catch(console.error);});await contentIslandClient.refreshSnapshot(); // initial load})().catch(err => {// Don't cache the failure: if the snapshot doesn't exist in Redis yet,// the next request retries instead of reusing a rejected promise.globalForClient.primed = null;throw err;});}return globalForClient.primed;} -
Create the publisher: it exports, does
SETandPUBLISH. Outside Next.js, it loads the.envwithdotenv:scripts/publish-redis.mts import 'dotenv/config';import { exportSnapshot } from '@content-island/api-client';import { createClient as createRedis } from 'redis';const redis = createRedis({ url: process.env.REDIS_URL });await redis.connect();const snapshot = await exportSnapshot({ accessToken: process.env.CONTENT_ISLAND_TOKEN! });await redis.set('content-island:snapshot', JSON.stringify(snapshot));await redis.publish('content-island:updated', '1');await redis.quit();console.log('✅ snapshot published to Redis');Add a script to your
package.jsonto launch it:package.json "scripts": {"publish:redis": "tsx scripts/publish-redis.mts"} -
Try it with two instances (that’s the whole point). Do a first load into Redis and start two apps on different ports:
Terminal window npm run publish:redis # first load into RedisPORT=3000 npm run dev # in one terminalPORT=3001 npm run dev # in anotherPublish new content and run
npm run publish:redisjust once. Reload both pages: both update theirexportedAtat the same time, with a singlePUBLISH.
Example
The examples repository implements the pattern in TanStack Start; the steps above are the adaptation to Next.js (only the framework glue changes): content-island/examples-dynamic-snapshot → 03-redis-pub-sub.