Redis pub/sub
A Dynamic Snapshot serves all your content from server memory and hot-refreshes it when you publish.
No rebuild. No API call per read.
The pattern has two pieces:
- Where the snapshot is read from → the
snapshotLoader. - 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: pub/sub instead of one
POSTper instance.
A single PUBLISH notifies all subscribed replicas, and each one calls refreshSnapshot().
When to choose it?
Choose it for horizontal scaling: refresh N instances with a single message, without per-instance webhooks or N API calls.
You need 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 # to run the TypeScript publisher.env REDIS_URL=redis://localhost:6379 -
Create the Redis connection:
src/server/redis.ts import 'dotenv/config';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:src/server/content-island.ts const SNAPSHOT_KEY = 'content-island:snapshot';const CHANNEL = 'content-island:updated';// The client's snapshotLoader: reads the JSON from RedissnapshotLoader: async () => {await redisReady;return (await redis.get(SNAPSHOT_KEY)) ?? '';},// Subscription + initial loadlet primed: Promise<unknown> | null = null;export function ensureSnapshot() {if (!primed) {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})();}return primed;} -
Create the publisher: it exports, does
SETandPUBLISH:scripts/publish-redis.ts 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(); -
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 set -a; source .env; set +anpx tsx scripts/publish-redis.ts # first load into RedisPORT=3000 npm run dev # in one terminalPORT=3001 npm run dev # in anotherPublish new content and run the publisher just once. Reload both pages: both update their
exportedAtat the same time, with a singlePUBLISH.
Example
Full code (includes the docker-compose.yml): content-island/examples-dynamic-snapshot → 03-redis-pub-sub.