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.
How It Works
Section titled “How It Works”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.
Requirements
Section titled “Requirements”- 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)
Configuring Chatto for Multiple Replicas
Section titled “Configuring Chatto for Multiple Replicas”Disable the embedded NATS server and point all server processes at your external cluster:
# Disable embedded NATSCHATTO_NATS_EMBEDDED_ENABLED=false
# Connect to external NATS clusterCHATTO_NATS_CLIENT_URL=nats://nats:4222CHATTO_NATS_CLIENT_AUTH_METHOD=tokenCHATTO_NATS_CLIENT_TOKEN=your-shared-token
# These MUST be identical across all server processesCHATTO_WEBSERVER_COOKIE_SIGNING_SECRET=your-cookie-secretCHATTO_WEBSERVER_COOKIE_ENCRYPTION_SECRET=your-cookie-encryption-secretCHATTO_CORE_SECRET_KEY=your-core-secretCHATTO_CORE_ASSETS_SIGNING_SECRET=your-assets-secret[nats.embedded]enabled = false
[nats.client]url = "nats://nats:4222"auth_method = "token"token = "your-shared-token"Health Checks
Section titled “Health Checks”Chatto exposes two health endpoints for orchestrator integration:
| Endpoint | Purpose | Returns 200 when |
|---|---|---|
GET /healthz | Liveness probe | Process is alive |
GET /readyz | Readiness probe | NATS 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.
Docker Compose
Section titled “Docker Compose”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: 3Scale up or down at any time:
docker compose up -d --scale chatto=5Kubernetes
Section titled “Kubernetes”readinessProbe: httpGet: path: /readyz port: 4000 periodSeconds: 5livenessProbe: httpGet: path: /healthz port: 4000 periodSeconds: 10startupProbe: httpGet: path: /readyz port: 4000 failureThreshold: 15 periodSeconds: 2Voice 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:
CHATTO_LIVEKIT_SERVER_ID=server-1The 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.
Load Balancing
Section titled “Load Balancing”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.
What You Need to Keep Consistent
Section titled “What You Need to Keep Consistent”| Setting | Must match across server processes? | Notes |
|---|---|---|
| NATS client URL & credentials | Yes | All connect to the same cluster |
cookie_signing_secret | Yes | Cookie validation |
cookie_encryption_secret | Yes, when configured | Cookie confidentiality |
core.secret_key | Yes | Bearer-token and account-flow verifier keys |
core.assets.signing_secret | Yes | Asset URL signing |
webserver.url | Yes | Public URL for absolute links |
livekit.server_id | Yes, within one Chatto server | Use a different value only across Chatto servers sharing one LiveKit cluster |