Scaling: Single Instance → Distributed

When you don’t need to scale

A single svelte-adapter-uws instance handles tens of thousands of concurrent WebSocket connections. For most apps, one server is enough.

You don’t need Redis or Postgres extensions if:

  • You run a single instance
  • All state can live in-memory
  • You don’t need data to survive restarts

When you need to scale

Add extensions when you need:

  • Multiple instances behind a load balancer → Redis pub/sub for cross-instance messaging
  • Persistent replay → Redis or Postgres replay buffer so messages survive restarts
  • Cross-instance presence → Redis presence for accurate “who’s online” across your fleet
  • Distributed rate limiting → Redis rate limiter for per-user limits across all instances

Step 1: Add Redis pub/sub

The most common first step. Replace in-memory pub/sub with Redis so platform.publish() reaches all instances.

npm install svelte-adapter-uws-extensions ioredis
// src/lib/server/redis.js
import { createRedisClient, createPubSub } from 'svelte-adapter-uws-extensions/redis';

export const redis = createRedisClient({ url: process.env.REDIS_URL });
export const bus = createPubSub(redis);
// src/hooks.ws.js
import { bus } from '$lib/server/redis';

export function open(ws, { platform }) {
  bus.activate(platform);
}

export const message = createMessage({
  platform: (p) => bus.wrap(p)
});

Now run multiple replicas:

# With SO_REUSEPORT, multiple instances share the same port
SO_REUSEPORT=1 node build &
SO_REUSEPORT=1 node build &

Step 2: Add persistent replay

Messages published while a client is disconnected are lost with in-memory replay. Redis replay buffers persist across restarts.

import { createReplay } from 'svelte-adapter-uws-extensions/redis';

export const replay = createReplay(redis, { maxPerTopic: 200 });

Step 3: Add cross-instance presence

In-memory presence only sees connections on the local instance. Redis presence aggregates across all instances.

import { createPresence } from 'svelte-adapter-uws-extensions/redis';

export const presence = createPresence(redis, {
  heartbeatInterval: 15000,
  expireAfter: 30000
});

Step 4: Add distributed rate limiting

import { createRateLimit } from 'svelte-adapter-uws-extensions/redis';

export const limiter = createRateLimit(redis, {
  points: 30,
  interval: 10000
});

Step 5: Postgres NOTIFY bridge (optional)

If you want database changes to trigger WebSocket events:

import { createPgClient, createNotifyBridge } from 'svelte-adapter-uws-extensions/postgres';

const pg = createPgClient({ connectionString: process.env.DATABASE_URL });
const bridge = createNotifyBridge(pg, { channel: 'table_changes' });
bridge.activate(platform);

Architecture

                +-- Instance 1 --+
Clients <--->   |  adapter-uws   |  <---+---> Redis
                +----------------+      |    (pub/sub, presence,
                                        |     replay, rate limit)
                +-- Instance 2 --+      |
Clients <--->   |  adapter-uws   |  <---+
                +----------------+
                                        |
                                        v
                                    Postgres
                                   (persistent data,
                                    NOTIFY bridge)

Each instance handles its own WebSocket connections. Redis ensures messages published on one instance reach subscribers on all instances.

Clustering

svelte-realtime works with the adapter’s CLUSTER_WORKERS mode. The adapter spawns N worker threads (default: number of CPUs). On Linux, workers share the port via SO_REUSEPORT and the kernel distributes incoming connections. On macOS and Windows, a primary thread accepts connections and routes them to workers via uWS child app descriptors.

Cross-worker ctx.publish() calls are batched via microtask coalescing - all publishes within one event loop tick are bundled into a single postMessage to the primary thread, which fans them out to other workers. This keeps IPC overhead constant regardless of publish volume.

Workers are health-checked every 10 seconds. If a worker fails to respond within 30 seconds, it is terminated and restarted with exponential backoff (starting at 100ms, max 5s, up to 50 restart attempts before the process exits). On graceful shutdown (SIGTERM / SIGINT), the primary stops accepting connections, sends a shutdown signal to all workers, and waits for them to drain in-flight requests and close WebSocket connections with code 1001 (Going Away) so clients reconnect to another instance.

MethodCross-worker?Notes
ctx.publish()Yes (relayed)Always safe
platform.send()N/A (single ws)Always safe
platform.sendTo()No (local only)Use publish with a user-specific topic instead
platform.subscribers()No (local only)Returns count for local worker only
platform.connectionsNo (local only)Returns count for local worker only

Was this page helpful?