Distributed Pub/Sub with Redis
Goal: Run multiple app replicas with cross-instance messaging.
One declaration of cluster intent should reach every framework publish surface: RPC ctx.publish, the cron tick, the reactive watcher path (live.effect / live.derived / live.aggregate / live.webhook), and the top-level publish() helper. Since 0.5.6 a single realtime({ bus, leader }) call wires all four.
Install
npm install svelte-adapter-uws-extensions ioredis Set up the Redis client and bus
// src/lib/server/redis.js
import { createRedis, createPubSubBus, createLeader } from 'svelte-adapter-uws-extensions/redis';
export const redis = createRedis({
url: process.env.REDIS_URL || 'redis://localhost:6379'
});
export const bus = createPubSubBus(redis);
export const leader = createLeader(redis); Wire it into hooks - recommended
// src/hooks.ws.js
import { realtime } from 'svelte-realtime/server';
import { bus, leader } from '$lib/server/redis';
export const { open, close, message, init } = realtime({
bus,
leader: leader.isLeader
});
export function upgrade() {
return { id: crypto.randomUUID() };
} That’s the whole file. realtime() returns the standard adapter hook set (open, close, message, init) and wires setBus, configureCron({ leader }), setCronPlatform, and _activateDerived for you when the adapter’s init({ platform }) lifecycle hook fires. Handler code (src/live/*.js) is byte-identical between single-replica and cluster - you opt in by passing bus and leader at the top of hooks.ws.js, nothing else changes.
For single-replica it’s the same file with no config:
// src/hooks.ws.js (single-replica)
import { realtime } from 'svelte-realtime/server';
export const { open, close, message, init } = realtime();
export function upgrade() { return { id: crypto.randomUUID() }; } Layer-1: manual wiring (experts)
realtime() is sugar over the existing primitives. For fine control - per-route bus, custom hook composition, conditional cluster wiring - drop down to the building blocks. As of 0.5.6 the manual pattern routes through the same compose-at-publish-time pipeline, so reactive handlers pick up cluster relay automatically once a bus is wired:
// src/hooks.ws.js (manual primitives)
import {
setBus, setCronPlatform, _activateDerived,
configureCron, pushHooks, message
} from 'svelte-realtime/server';
import { bus, leader } from '$lib/server/redis';
setBus(bus); // process-wide bus
configureCron({ leader: leader.isLeader });
export { message };
export const open = pushHooks.open;
export const close = pushHooks.close;
export function init({ platform }) {
setCronPlatform(platform);
_activateDerived(platform);
}
export function upgrade() {
return { id: crypto.randomUUID() };
} setBus(bus) and configureCron({ bus }) write the same backing state; pick whichever reads better at the call site.
Cluster bus reaches every publish surface
Before 0.5.6, the reactive seam (live.effect, live.derived, live.aggregate, live.webhook) silently delivered local-only on multi-replica deployments because the reactive wrap captured the raw adapter platform at activation time, with no bus indirection. A leader-gated effect on replica A that published audit / notifications reached only ~50% of users (whichever ones were holding their WS on replica A). 0.5.6 switched the reactive wrap to compose-at-publish-time so a single setBus(bus) reaches the reactive seam too. realtime({ bus, leader }) is the single-call way to opt in.
Why a platform: (p) => bus.wrap(p) callback is no longer recommended
Pre-0.5.6, the canonical pattern was bus.activate(platform) in open plus createMessage({ platform: (p) => bus.wrap(p) }). That worked for RPC but missed the reactive seam, and as of 0.5.7 it triggers a one-shot dev warn when a process-wide bus is also configured (the framework can detect that user-built platform callbacks double-relay). The fix is to drop the callback - setBus(bus) (or realtime({ bus })) is the single source of truth.
- export const message = createMessage({
- platform: (p) => bus.wrap(p)
- });
+ // realtime({ bus, leader }) above already wired setBus(bus) for you;
+ // bare createMessage() / the framework's default `message` export
+ // auto-wraps with whatever setBus configured. Run multiple replicas
CLUSTER_MODE=reuseport WORKERS=4 node build &
CLUSTER_MODE=reuseport WORKERS=4 node build & Both instances share port 443 via SO_REUSEPORT (Linux); on Windows / macOS use the acceptor cluster mode. Redis forwards publishes between them, the leader gate fires cron jobs once across the cluster, and reactive handlers (live.effect, etc.) deliver fan-out across replicas.
Combined: Redis + rate limiting
realtime() returns the standard hook set; for the cross-cutting beforeExecute rate-limit gate, swap realtime()’s message for a createMessage you composed yourself. The bus is still wired once via setBus (or the realtime({ bus }) call you make alongside), so the reactive seam and cron tick stay cluster-correct without a per-hook bus callback:
import { realtime, createMessage, setBus, LiveError } from 'svelte-realtime/server';
import { createRedis, createPubSubBus, createLeader, createRateLimit } from 'svelte-adapter-uws-extensions/redis';
const redis = createRedis();
const bus = createPubSubBus(redis);
const leader = createLeader(redis);
const limiter = createRateLimit(redis, { points: 30, interval: 10000 });
setBus(bus);
export const { open, close, init } = realtime({ leader: leader.isLeader });
export function upgrade() { return { id: crypto.randomUUID() }; }
export const message = createMessage({
async beforeExecute(ws, rpcPath) {
const { allowed, resetMs } = await limiter.consume(ws);
if (!allowed)
throw new LiveError('RATE_LIMITED', `Retry in ${Math.ceil(resetMs / 1000)}s`);
}
}); Without a platform callback, createMessage auto-wraps with whatever setBus(...) wired - one source of truth, no double-wrap.
Related guides
- Scaling Guide - step-by-step from single instance to distributed
- Scaling and Resilience - when and why you need extensions
- Replay and Presence - persistent replay and cross-instance presence
- Observability - monitoring and metrics
- Extensions package - full API reference
Was this page helpful?