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 server process 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 server processes connect to the same external NATS cluster. NATS JetStream handles:

  • Persistent storage — messages, users, 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 server process 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 server process)
  • A load balancer in front of the Chatto replicas (Caddy, nginx, Traefik, etc.)
  • The same secrets across all server processes (cookie signing, cookie encryption if configured, core token verifier key, asset signing)

Disable the embedded NATS server and point all server processes 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 server processes
CHATTO_WEBSERVER_COOKIE_SIGNING_SECRET=your-cookie-secret
CHATTO_WEBSERVER_COOKIE_ENCRYPTION_SECRET=your-cookie-encryption-secret
CHATTO_CORE_SECRET_KEY=your-core-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 a server process that hasn’t finished connecting to NATS.

chatto:
image: ghcr.io/chattocorp/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 Server Processes

Section titled “Voice and Video Calls with Multiple Server Processes”

If you’re using LiveKit for calls, set the same server_id on every replica of one Chatto server. Use a different value only for different Chatto servers that share the same LiveKit cluster:

Terminal window
CHATTO_LIVEKIT_SERVER_ID=server-1

The server_id is prefixed to deterministic LiveKit room names, allowing the webhook handler to identify which Chatto server owns each event. If replicas of the same Chatto server use different values, users routed to different replicas can receive tokens for different LiveKit rooms for the same Chatto room.

Chatto is fully stateless, so load balancing is straightforward:

  • Round-robin is the recommended strategy
  • Session affinity is not required — any server process 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 server processes?Notes
NATS client URL & credentialsYesAll connect to the same cluster
cookie_signing_secretYesCookie validation
cookie_encryption_secretYes, when configuredCookie confidentiality
core.secret_keyYesBearer-token and account-flow verifier keys
core.assets.signing_secretYesAsset URL signing
webserver.urlYesPublic URL for absolute links
livekit.server_idYes, within one Chatto serverUse a different value only across Chatto servers sharing one LiveKit cluster