Skip to Content
GuidesServer GuidesReal-Time Sync

Real-Time Sync

Enable instant updates across all connected clients with WebSocket and Redis broadcasting.

How It Works

Client A Server Pod 1 Redis Server Pod 2 Client B │ │ │ │ │ │ 1. Mutation │ │ │ │ ├────POST /tx────────────>│ │ │ │ │ │ │ │ │ │ │ 2. Write SyncAction │ │ │ │ ├─────to Database──────┤ │ │ │ │ │ │ │ │ │ 3. Publish Frame │ │ │ │ ├──────PUBLISH─────────>│ │ │ │ │ │ 4. Broadcast │ │ │ │ ├──────SUBSCRIBE───────>│ │ │ │ │ │ 5. Push to WebSocket │ │ │ │ ├──────WS Frame─────────>│ │ 6. Response │ │ │ │ │<────{ ok: true }────────┤ │ │ │ │ │ │ │ │

Single Server (Default)

WebSocket works automatically without additional configuration:

import { SyncServer } from '@gluonic/server' const server = SyncServer({ database, auth: JWTAuth({ secret: process.env.JWT_SECRET }) // No broadcaster needed - WebSocket works for this pod! })

Behavior:

  • ✅ WebSocket connections to this pod receive updates
  • ❌ WebSocket connections to other pods don’t receive updates
  • Use case: Development, single-server production

With Broadcaster (Production)

Add Redis broadcaster for multi-pod deployments and cross-service updates:

import { SyncServer } from '@gluonic/server' import { RedisBroadcaster } from '@gluonic/broadcaster-redis' const server = SyncServer({ database, auth: JWTAuth({ secret: process.env.JWT_SECRET }), broadcaster: RedisBroadcaster({ url: 'redis://localhost:6379' }) })

API Reference: RedisBroadcaster - Broadcasting configuration

Behavior:

  • ✅ Changes broadcast to ALL pods via Redis pub/sub
  • ✅ ALL connected clients receive updates (any pod)
  • ✅ Horizontal scaling works!

Configuration

Redis URL Formats

// Local development 'redis://localhost:6379' // With password 'redis://:password@localhost:6379' // AWS ElastiCache 'redis://my-cluster.abc123.use1.cache.amazonaws.com:6379' // With TLS 'rediss://:password@my-cluster.abc123.use1.cache.amazonaws.com:6379'

Auto-Detection from Environment

const redisUrl = process.env.REDIS_URL || (process.env.REDIS_ENDPOINT && process.env.REDIS_PORT ? `redis://${process.env.REDIS_ENDPOINT}:${process.env.REDIS_PORT}` : null) if (redisUrl) { broadcaster = makeRedisBroadcaster(redisUrl) }

Client Connection

WebSocket Setup (Automatic)

Clients connect via createSyncClient():

const { store } = createSyncClient({ server: { http: 'https://api.example.com/sync/v1', websocket: 'wss://api.example.com/sync/v1' // Auto-appends /ws }, // ... other config })

Connection Lifecycle

// 1. Client calls store.catchUp() await store.catchUp() // 2. WebSocket connects WebSocket → wss://api.example.com/sync/v1/ws?token=JWT_TOKEN // 3. Server authenticates auth(req) // Verifies token // 4. Connection established // Client added to org's socket pool // 5. Frames pushed automatically // Server → Client via WebSocket // 6. Client applies frames store.applyFrames(frames) // UI updates in real-time ✓

Broadcast Flow

When Mutation Happens

// 1. Client sends mutation POST /sync/v1/tx { ops: [{ t: 'post', id: '123', op: 'u', patch: { title: 'New' } }] } // 2. Server writes SyncAction await adapter.writeSyncAction(orgId, { model: 'post', modelId: '123', op: 'u', patch: { title: 'New' }, actorId: userId }) // 3. Server publishes to Redis await broadcaster.publish(orgId, [{ sid: 15, t: 'post', id: '123', op: 'u', p: { title: 'New' }, who: userId, at: Date.now() }]) // 4. ALL pods receive via Redis subscription // Each pod pushes to its local WebSocket clients // 5. Clients apply frame // UI updates across all devices ✓

Multi-Tab Sync

With Redis, multi-tab sync works automatically:

// Browser Tab 1 await store.save('post', '123', { title: 'Tab 1 Edit' }) // Broadcast flow: // Tab 1 → Server → Redis → Server → Tab 2 // Browser Tab 2 (same browser!) // Receives frame via WebSocket // UI updates automatically ✓

Heartbeat & Reconnection

Server Heartbeat

// Server sends heartbeat every 10 seconds setInterval(() => { for (const ws of sockets) { ws.ping() // WebSocket protocol ping ws.send(JSON.stringify({ type: 'heartbeat' })) // App-level heartbeat } }, 10000)

Client Reconnection

// Client handles disconnections automatically ws.onclose = () => { console.log('WebSocket disconnected, reconnecting...') // Exponential backoff setTimeout(() => { reconnect() }, Math.min(1000 * Math.pow(2, attempt), 30000)) } // After reconnecting: // - Delta sync catches up on missed changes // - WebSocket resumes real-time updates

Performance

Frame Batching

Large batches are chunked to avoid WebSocket limits:

// Server config const MAX_FRAMES_PER_MESSAGE = 500 // If 2000 frames to broadcast: // Chunk 1: frames 0-499 // Chunk 2: frames 500-999 // Chunk 3: frames 1000-1499 // Chunk 4: frames 1500-1999 // Sent as separate WebSocket messages // Client processes chunks sequentially

Backpressure

Client-side backpressure prevents overload:

// Client queues incoming frames const frameQueue = [] ws.onmessage = (evt) => { const frames = JSON.parse(evt.data) frameQueue.push(...frames) } // Process queue at controlled rate setInterval(() => { const batch = frameQueue.splice(0, 100) if (batch.length > 0) { store.applyFrames(batch) } }, 100)

Monitoring

Connection Count

// Get current WebSocket connections const metrics = await fetch('http://localhost:3000/sync/v1/metrics') .then(r => r.json()) console.log('Active connections:', metrics.wsConnections)

Broadcast Metrics

{ "wsConnections": 42, // Active WebSocket clients "wsBroadcastedFrames": 1500, // Total frames sent "publishLatencyMsTotal": 2500, // Total latency "publishLatencyCount": 1500 // Avg = 1.67ms per frame }

Troubleshooting

WebSocket connections immediately close

Problem: Connections open then close within 1 second.

Solutions:

  • Check auth function handles WebSocket token (query param)
  • Verify Redis is running (if using broadcaster)
  • Check firewall allows WebSocket (ws/wss protocol)
  • Check CORS allows WebSocket upgrade

No real-time updates

Problem: Mutations succeed but other clients don’t update.

Solutions:

  • Verify Redis broadcaster is configured
  • Check Redis connection is working
  • Ensure all pods subscribe to Redis
  • Check WebSocket connections are established

Redis connection errors

Problem: Error: Redis connection failed

Solutions:

  • Verify Redis URL is correct
  • Check Redis server is running
  • Test connection: redis-cli ping
  • Check network/firewall rules

Next Steps

Last updated on