Skip to content

Horizontal Scaling

By default, Chatto runs as a single process with an embedded NATS server — no external dependencies needed. This is the simplest way to run Chatto and works well for small to medium teams.

When you outgrow a single process, Chatto scales horizontally. Every instance is stateless — all persistent data lives in a shared NATS cluster. You can run as many replicas as you need behind a load balancer, and they’ll transparently share data.

STATELESS REPLICAS BrowserSvelteKit SPA CaddyLoad Balancer Chattoreplica 1 Chattoreplica 2 Chattoreplica 3 NATS JetStream shared state

All Chatto instances connect to the same external NATS cluster. NATS JetStream handles:

  • Persistent storage — messages, users, spaces, memberships, and configuration
  • Real-time events — subscriptions, presence updates, and call state
  • File storage — attachments (unless offloaded to S3)

Because there’s no local state, any instance can handle any request. No session affinity is required — round-robin load balancing works perfectly.

  • An external NATS cluster (embedded NATS only supports a single instance)
  • A load balancer in front of the Chatto replicas (Caddy, nginx, Traefik, etc.)
  • The same secrets across all instances (cookie signing, asset signing)

Disable the embedded NATS server and point all instances at your external cluster:

Terminal window
# Disable embedded NATS
CHATTO_NATS_EMBEDDED_ENABLED=false
# Connect to external NATS cluster
CHATTO_NATS_CLIENT_URL=nats://nats:4222
CHATTO_NATS_CLIENT_AUTH_METHOD=token
CHATTO_NATS_CLIENT_TOKEN=your-shared-token
# These MUST be identical across all instances
CHATTO_WEBSERVER_COOKIE_SIGNING_SECRET=your-cookie-secret
CHATTO_CORE_ASSETS_SIGNING_SECRET=your-assets-secret

Chatto exposes two health endpoints for orchestrator integration:

EndpointPurposeReturns 200 when
GET /healthzLiveness probeProcess is alive
GET /readyzReadiness probeNATS is connected and JetStream is ready

Use /readyz for startup and readiness probes to ensure traffic isn’t routed to an instance that hasn’t finished connecting to NATS.

chatto:
image: ghcr.io/hmans/chatto:latest
deploy:
replicas: 3
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:4000/readyz"]
interval: 5s
timeout: 3s
retries: 3

Scale up or down at any time:

Terminal window
docker compose up -d --scale chatto=5
readinessProbe:
httpGet:
path: /readyz
port: 4000
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: 4000
periodSeconds: 10
startupProbe:
httpGet:
path: /readyz
port: 4000
failureThreshold: 15
periodSeconds: 2

Voice and Video Calls with Multiple Instances

Section titled “Voice and Video Calls with Multiple Instances”

If you’re using LiveKit for calls, set a unique instance_id on each Chatto instance so LiveKit webhooks are routed correctly:

Terminal window
CHATTO_LIVEKIT_INSTANCE_ID=instance-1

The instance ID is prefixed to LiveKit room names, allowing the webhook handler to identify which Chatto instance should process each event. Without it, webhooks may be misrouted when multiple instances share a LiveKit cluster.

Chatto is fully stateless, so load balancing is straightforward:

  • Round-robin is the recommended strategy
  • Session affinity is not required — any instance can serve any request
  • WebSocket connections are long-lived; ensure your load balancer supports them

The Docker Compose deployment guide includes a Caddy configuration that handles this automatically.

SettingMust match across instances?Notes
NATS client URL & credentialsYesAll connect to the same cluster
cookie_signing_secretYesCookie validation
assets_signing_secretYesAsset URL signing
webserver.urlYesPublic URL for absolute links
livekit.instance_idNo — must be uniqueWebhook routing