Resilience & Testing
Circuit Breaker
Prevents thundering herd when a backend goes down. When Redis or Postgres becomes unreachable, every extension that uses the breaker fails fast instead of queueing up timeouts, and fire-and-forget operations (heartbeats, relay flushes, cursor broadcasts) are skipped entirely.
Three states:
- healthy - everything works, requests go through
- broken - too many failures, requests fail fast via
CircuitBrokenError - probing - one request is allowed through to test if the backend is back
Setup
// src/lib/server/breaker.js
import { createCircuitBreaker } from 'svelte-adapter-uws-extensions/breaker';
export const breaker = createCircuitBreaker({
failureThreshold: 5,
resetTimeout: 30000,
onStateChange: (from, to) => console.log(`circuit: ${from} -> ${to}`)
}); Pass the same breaker to all extensions that share a backend:
import { breaker } from './breaker.js';
export const bus = createPubSubBus(redis, { breaker });
export const presence = createPresence(redis, { breaker, key: 'id' });
export const replay = createReplay(redis, { breaker });
export const limiter = createRateLimit(redis, { points: 10, interval: 1000, breaker }); Failures from any extension contribute to the same breaker. When one trips it, all others fail fast.
Options
| Option | Default | Description |
|---|---|---|
failureThreshold | 5 | Consecutive failures before breaking |
resetTimeout | 30000 | Ms before transitioning from broken to probing |
onStateChange | - | Called on state transitions: (from, to) => void |
API
| Method / Property | Description |
|---|---|
breaker.state | 'healthy', 'broken', or 'probing' |
breaker.isHealthy | true only when state is 'healthy' |
breaker.failures | Current consecutive failure count |
breaker.guard() | Throws CircuitBrokenError if the circuit is broken |
breaker.success() | Record a successful operation |
breaker.failure() | Record a failed operation |
breaker.reset() | Force back to healthy |
breaker.destroy() | Clear internal timers |
How extensions use it
Awaited operations (join, consume, publish) call guard() before the Redis/Postgres call, success() after, and failure() in the catch block. When the circuit is broken, guard() throws CircuitBrokenError and the operation never reaches the backend.
Fire-and-forget operations (heartbeat refresh, relay flush, cursor broadcast) check isHealthy and skip entirely when the circuit is not healthy. This prevents piling up commands on a dead connection.
Error handling
import { CircuitBrokenError } from 'svelte-adapter-uws-extensions/breaker';
try {
await replay.publish(platform, 'chat', 'msg', data);
} catch (err) {
if (err instanceof CircuitBrokenError) {
// Backend is down -- degrade gracefully
platform.publish('chat', 'msg', data); // local-only delivery
}
} Graceful Shutdown
All clients listen for the sveltekit:shutdown event and disconnect cleanly by default. You can disable this with autoShutdown: false and manage the lifecycle yourself.
// Manual shutdown
await redis.quit();
await pg.end();
presence.destroy();
cursors.destroy();
lobby.destroy();
breaker.destroy(); Call destroy() on any extension that runs background timers (presence heartbeats, cursor throttle timers, circuit breaker reset timers, Postgres cleanup intervals). The client quit()/end() methods close the underlying connections.
Testing
Tests use in-memory mocks for Redis and Postgres - no running services needed.
npm test Since the extensions match the core plugin APIs, you can swap between in-memory plugins (for tests and single-instance dev) and Redis/Postgres extensions (for production) by changing the import path. Structure your code so the extension instance is created in a single file (src/lib/server/replay.js, etc.) and imported everywhere else. To switch backends, change that one file.
Was this page helpful?