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.
How It Works
Section titled “How It Works”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.
Requirements
Section titled “Requirements”- 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)
Configuring Chatto for Multiple Replicas
Section titled “Configuring Chatto for Multiple Replicas”Disable the embedded NATS server and point all instances 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 instancesCHATTO_WEBSERVER_COOKIE_SIGNING_SECRET=your-cookie-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 an instance that hasn’t finished connecting to NATS.
Docker Compose
Section titled “Docker Compose”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: 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 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:
CHATTO_LIVEKIT_INSTANCE_ID=instance-1The 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.
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 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.
What You Need to Keep Consistent
Section titled “What You Need to Keep Consistent”| Setting | Must match across instances? | Notes |
|---|---|---|
| NATS client URL & credentials | Yes | All connect to the same cluster |
cookie_signing_secret | Yes | Cookie validation |
assets_signing_secret | Yes | Asset URL signing |
webserver.url | Yes | Public URL for absolute links |
livekit.instance_id | No — must be unique | Webhook routing |