Skip to content

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?

Diagram: Dynamic Snapshot in TanStack Start with Redis pub/sub. A publisher does SET + PUBLISH and all subscribed instances refresh at once with a single message
  • Source: Redis. The snapshotLoader reads the snapshot key.
  • Trigger: pub/sub instead of one POST per 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.

  1. Start Redis and install the dependencies:

    Terminal window
    docker run -d --name redis -p 6379:6379 redis:7
    npm i redis
    npm i -D tsx # to run the TypeScript publisher
    .env
    REDIS_URL=redis://localhost:6379
  2. 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();
  3. Change the snapshotLoader to 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 Redis
    snapshotLoader: async () => {
    await redisReady;
    return (await redis.get(SNAPSHOT_KEY)) ?? '';
    },
    // Subscription + initial load
    let 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;
    }
  4. Create the publisher: it exports, does SET and PUBLISH:

    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();
  5. 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 +a
    npx tsx scripts/publish-redis.ts # first load into Redis
    PORT=3000 npm run dev # in one terminal
    PORT=3001 npm run dev # in another

    Publish new content and run the publisher just once. Reload both pages: both update their exportedAt at the same time, with a single PUBLISH.

Example

Full code (includes the docker-compose.yml): content-island/examples-dynamic-snapshot03-redis-pub-sub.

References