How to Scale Kaneo's WebSocket Broadcasting Horizontally

To scale Kaneo's WebSocket broadcasting horizontally, configure the REDIS_URL environment variable to enable RedisBroadcastAdapter, which synchronizes real-time events across multiple API instances using Redis Pub/Sub.

Kaneo's real-time collaboration features rely on WebSocket connections to deliver live updates to connected clients. While the default implementation works for single-instance deployments, production workloads require horizontal scaling to handle high concurrency. The solution leverages a pluggable broadcast adapter architecture that replaces in-memory messaging with Redis-backed message distribution.

Understanding Kaneo's Broadcast Adapter Architecture

The WebSocket layer in apps/api/src/ws/ implements a clean adapter pattern defined in broadcast-adapter.ts. This interface abstracts the underlying message transport, allowing the system to switch between single-instance and distributed modes without changing application code.

InMemoryBroadcastAdapter serves as the default fallback when no external message broker is configured. This adapter maintains WebSocket connections in local memory and broadcasts events only to clients connected to the same process instance. While efficient for development or low-traffic deployments, it cannot propagate messages across multiple API replicas.

RedisBroadcastAdapter (located in redis-broadcast-adapter.ts) solves this limitation by integrating with Redis Pub/Sub. When enabled, each API instance publishes messages to shared Redis channels while simultaneously subscribing to messages from peer instances. This ensures that a broadcast initiated on one server reaches WebSocket clients connected to any other server in the cluster.

How Redis Pub/Sub Enables Horizontal Scaling

The RedisBroadcastAdapter implements a sophisticated message routing protocol that guarantees exactly-once delivery per client while maintaining low latency.

Instance Identification and Channel Structure

Each API instance generates a unique instance ID using randomUUID() during startup. The adapter subscribes to two distinct channel patterns in Redis:

  • kaneo:ws-project:<project-id>:broadcast – for project-scoped collaboration events
  • kaneo:ws-user:<user-id>:broadcast – for user-specific notifications

When your application calls broadcastToProject(projectId, payload) or broadcastToUser(userId, payload), the adapter executes a two-phase delivery:

  1. Local delivery: The message transmits immediately to WebSocket connections existing on the current instance.
  2. Global propagation: The adapter publishes the payload to the appropriate Redis channel, prefixed with the origin instance ID.

Duplicate Prevention Mechanism

Subscribing instances receive all Pub/Sub messages but implement origin filtering to prevent echo effects. The adapter compares the origin field in each incoming message against its own instance ID. Messages originating from the local instance are discarded, while external messages are forwarded to local WebSocket clients. This de-duplication logic ensures clients receive exactly one copy of each event regardless of cluster topology.

Step-by-Step Configuration Guide

Follow these steps to enable horizontal WebSocket broadcasting in your Kaneo deployment:

  1. Provision Redis infrastructure: Deploy a Redis instance (version 6.0 or higher recommended) using Docker, managed cloud services, or a Redis Cluster for high availability. Ensure network connectivity between your API instances and the Redis host.

  2. Configure environment variables: Set REDIS_URL to your Redis connection string (e.g., redis://redis:6379). Alternatively, use REDIS_HOST and REDIS_PORT depending on your deployment configuration. The function isRedisConfigured() in the source code detects these variables to instantiate RedisBroadcastAdapter instead of the in-memory version.

  3. Deploy multiple API replicas: Launch multiple containers or pods running ghcr.io/usekaneo/kaneo-api:latest behind a load balancer. Each instance calls initializeWebSocketAdapter() during startup (invoked from the Hono application initialization in apps/api/src/ws/index.ts), automatically entering clustered mode.

  4. Tune broadcast latency: Optionally adjust the internal debounce timeout (default 100ms) in redis-broadcast-adapter.ts to balance between message latency and Redis write volume based on your traffic patterns.

Source Code Implementation Details

The WebSocket initialization logic resides in apps/api/src/ws/index.ts, which orchestrates connection tracking and adapter selection:

// apps/api/src/ws/index.ts
await initializeWebSocketAdapter();   // Automatically selects RedisBroadcastAdapter when REDIS_URL is set

When REDIS_URL is present, the system instantiates RedisBroadcastAdapter which implements the interface defined in broadcast-adapter.ts. Your application code requires no modifications when switching adapters:

import { broadcastToUser } from '@kaneo/api/src/ws';

// This call works identically in single-instance or clustered modes
broadcastToUser('user-123', { type: 'NOTIFICATION_CREATED', data: { id: 456 } });

The underlying implementation ensures that when this executes on any API replica, the RedisBroadcastAdapter publishes to kaneo:ws-user:user-123:broadcast, and all other replicas deliver the payload to their local connections for that user.

For Docker Compose deployments, add the Redis service and environment configuration:

services:
  api:
    image: ghcr.io/usekaneo/kaneo-api:latest
    environment:
      - REDIS_URL=redis://redis:6379
    depends_on:
      - redis
    deploy:
      replicas: 3
  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

Summary

  • Broadcast Adapter Pattern: Kaneo uses broadcast-adapter.ts to abstract message transport, supporting seamless switching between InMemoryBroadcastAdapter and RedisBroadcastAdapter.
  • Redis Pub/Sub: Horizontal scaling requires REDIS_URL configuration to enable cross-instance message propagation via kaneo:ws-project and kaneo:ws-user channels.
  • Duplicate Filtering: Each instance uses a unique UUID to filter its own messages from Redis subscriptions, ensuring exactly-once delivery to WebSocket clients.
  • Zero Code Changes: Existing calls to broadcastToProject and broadcastToUser function identically in clustered configurations without application modifications.
  • Key Files: broadcast-adapter.ts (interface), redis-broadcast-adapter.ts (Redis implementation), in-memory-broadcast-adapter.ts (fallback), and index.ts (initialization).

Frequently Asked Questions

What is the difference between InMemoryBroadcastAdapter and RedisBroadcastAdapter?

InMemoryBroadcastAdapter stores WebSocket connections in process memory and broadcasts only to local clients, suitable for single-instance deployments. RedisBroadcastAdapter uses Redis Pub/Sub to synchronize messages across all API instances, enabling horizontal scaling but requiring an external Redis service.

How does Kaneo prevent duplicate WebSocket messages when using multiple instances?

The RedisBroadcastAdapter attaches a unique instance ID (generated via randomUUID()) to every published message. When instances receive messages from Redis, they compare the origin field against their own ID and discard messages that originated locally, forwarding only external messages to connected clients.

What Redis version or configuration does Kaneo require?

Kaneo requires Redis 6.0 or higher with Pub/Sub functionality enabled. Set the REDIS_URL environment variable to a standard Redis connection string. For production deployments, use Redis persistence or a managed service; clustering is supported as long as Pub/Sub commands are available to all API instances.

Do I need to modify my application code to support horizontal WebSocket scaling?

No. The broadcast adapter pattern abstracts the transport layer. Your existing code using broadcastToUser or broadcastToProject functions identically regardless of whether InMemoryBroadcastAdapter or RedisBroadcastAdapter is active. Simply setting REDIS_URL enables horizontal scaling without code changes.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →