Scaling Strategies
Guide for scaling Soothe deployments from single-node to large-scale production.
Scaling Tiers
Tier 1: Single Node (<50 concurrent threads)
- Architecture: Docker Compose, single PostgreSQL instance
- Limits: ~50 concurrent threads, ~10 parallel goals
- Reference: Production Setup
Tier 2: Multi-Node (<200 concurrent threads)
- Architecture: Load balancer + multiple daemon nodes + PostgreSQL replicas
- Limits: ~200 concurrent threads, ~30 parallel goals
- See below: Multi-node scaling section
Tier 3: Large Scale (<1000 concurrent threads)
- Architecture: Kubernetes + PostgreSQL cluster + Redis coordination
- Limits: ~1000 concurrent threads, ~100 parallel goals
- See below: Kubernetes scaling section
Horizontal Scaling: Multi-Node Deployment
Architecture
┌──────────────┐
│ Load Balancer│ (nginx/HAProxy)
└──────┬───────┘
│ Round-robin routing
├─→ ┌──────────────┐
│ │ Soothe Node 1│
│ └──────┬───────┘
│ │ PostgreSQL primary
├─→ ┌──────────────┐
│ │ Soothe Node 2│
│ └──────┬───────┘
│ │
└─────→ ┌──────────────┐
│ PostgreSQL │
│ Primary + │
│ Replicas │
└──────────────┘
Load Balancer Configuration (nginx)
Upstream configuration:
upstream soothe_daemons {
least_conn; # Route to least busy node
server soothe-node1:8765 weight=1;
server soothe-node2:8765 weight=1;
server soothe-node3:8765 weight=1;
health_check interval=10s fails=3 passes=2;
}
server {
listen 443 ssl;
location /ws {
proxy_pass http://soothe_daemons;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
PostgreSQL Scaling
Primary-replica setup:
# docker-compose.yml (PostgreSQL cluster)
services:
postgres-primary:
image: pgvector:pg17
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
command: ["postgres", "-c", "max_connections=300"]
postgres-replica1:
image: pgvector:pg17
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
command: |
postgres
-c primary_conninfo='host=postgres-primary port=5432 user=postgres password=${POSTGRES_PASSWORD}'
-c standby_mode=on
Connection pooling:
persistence:
postgres_pool_min_size: 16
checkpointer_pool_size: 48
sloop_pool_size: 48
Daemon Node Configuration
Each node needs unique workspace mounts:
# Node 1: workspace mapping
workspace_mount:
host_root: /mnt/shared/workspace
container_root: /var/lib/soothe/workspaces
# Node 2: same shared workspace
workspace_mount:
host_root: /mnt/shared/workspace
container_root: /var/lib/soothe/workspaces
Concurrency tuning:
agent:
autonomous:
max_parallel_goals: 10 # Per-node limit
max_loops: 8 # Worker pool size
loop:
concurrency:
max_parallel_tools: 30
llm_rate_limit:
concurrent_limit: 20
Kubernetes Deployment
Namespace and Secrets
apiVersion: v1
kind: Namespace
metadata:
name: soothe-production
---
apiVersion: v1
kind: Secret
metadata:
name: soothe-secrets
namespace: soothe-production
type: Opaque
data:
openai-api-key: <base64>
postgres-password: <base64>
PostgreSQL StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: soothe-postgres
namespace: soothe-production
spec:
serviceName: soothe-postgres
replicas: 3 # Primary + 2 replicas
selector:
matchLabels:
app: soothe-postgres
template:
metadata:
labels:
app: soothe-postgres
spec:
containers:
- name: postgres
image: pgvector:pg17
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: soothe-secrets
key: postgres-password
ports:
- containerPort: 5432
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: postgres-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 100Gi
Soothe Daemon Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: soothe-daemon
namespace: soothe-production
spec:
replicas: 3
selector:
matchLabels:
app: soothe-daemon
template:
metadata:
labels:
app: soothe-daemon
spec:
containers:
- name: soothed
image: soothed:latest
env:
- name: OPENAI_API_KEY
valueFrom:
secretKeyRef:
name: soothe-secrets
key: openai-api-key
- name: SOOTHE_POSTGRES_BASE_DSN
value: "postgresql://postgres:$(POSTGRES_PASSWORD)@soothe-postgres-0:5432"
ports:
- containerPort: 8765
volumeMounts:
- name: workspace
mountPath: /var/lib/soothe/workspaces
- name: config
mountPath: /var/lib/soothe/config
volumes:
- name: workspace
persistentVolumeClaim:
claimName: workspace-pvc
- name: config
configMap:
name: soothe-config
Horizontal Pod Autoscaler
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: soothe-daemon-hpa
namespace: soothe-production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: soothe-daemon
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
Service and Ingress
apiVersion: v1
kind: Service
metadata:
name: soothe-daemon-service
namespace: soothe-production
spec:
selector:
app: soothe-daemon
ports:
- port: 8765
targetPort: 8765
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: soothe-ingress
namespace: soothe-production
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
tls:
- hosts:
- soothe.your-domain.com
secretName: soothe-tls
rules:
- host: soothe.your-domain.com
http:
paths:
- path: /ws
pathType: Prefix
backend:
service:
name: soothe-daemon-service
port:
number: 8765
Performance Tuning
PostgreSQL Optimization
-- PostgreSQL configuration
ALTER SYSTEM SET max_connections = 300;
ALTER SYSTEM SET shared_buffers = '1GB';
ALTER SYSTEM SET work_mem = '64MB';
ALTER SYSTEM SET effective_cache_size = '3GB';
ALTER SYSTEM SET random_page_cost = 1.1; -- For SSDs
ALTER SYSTEM SET effective_io_concurrency = 200;
pgvector optimization:
-- HNSW index for fast vector search
CREATE INDEX ON soothe_embeddings USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- IVFFlat for larger datasets
CREATE INDEX ON soothe_embeddings USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
Daemon Pool Sizing
persistence:
postgres_pool_min_size: 16 # Base pool
checkpointer_pool_size: 48 # LangGraph checkpoints
sloop_pool_size: 48 # StrangeLoop state
agent:
autonomous:
max_loops: 8 # Worker pool per node
max_parallel_goals: 10 # Concurrent goal execution
loop:
concurrency:
max_parallel_tools: 30 # Tool call parallelism
llm_rate_limit:
concurrent_limit: 20 # LLM API concurrency
rpm_limit: 500 # Requests per minute
Pool sizing formula:
postgres_pool_min_size: 4 × number of nodescheckpointer_pool_size: 24 × number of nodesmax_loops: CPU cores × 2max_parallel_goals: max_loops ÷ 2
LLM Rate Limiting
Provider-specific limits:
agent:
loop:
llm_rate_limit:
rpm_limit: 500 # OpenAI limit
concurrent_limit: 20 # Max concurrent API calls
# Adaptive timeouts for rate limit handling
call_timeout_seconds: 600
retry_on_timeout: true
max_timeout_retries: 10
Provider rate limits (reference):
- OpenAI: 500 RPM (Tier 1), 5000 RPM (Tier 2)
- Anthropic: 1000 RPM default
- DashScope: 60 RPM (free), 600 RPM (paid)
Capacity Planning
Thread Capacity Estimation
Formula: Max concurrent threads = max_loops × max_parallel_goals × nodes
Example:
- 3 nodes × 8 loops × 10 parallel goals = 240 concurrent threads
Database capacity:
- PostgreSQL: 300 max_connections recommended
- Each thread: 2-3 database connections (checkpointer + metadata)
Memory Estimation
Per-thread memory: ~50 MB (StrangeLoop state + LLM context)
Total memory: Threads × 50 MB + Base daemon memory
Example:
- 100 threads × 50 MB = 5 GB
- Base daemon: 500 MB
- Total: 5.5 GB per node
Recommendation: 8 GB RAM per node for 100 threads
CPU Estimation
Per-thread CPU: 0.5-1.0 cores (LLM reasoning)
Total CPU: Threads × 0.5 cores
Example:
- 100 threads × 0.5 cores = 50 cores total
- 3 nodes: 17 cores per node
Recommendation: 8-16 cores per node
Storage Estimation
Thread storage: ~1-5 MB per thread (checkpoints + metadata)
Vector storage: Embedding size × vectors
Example (1000 threads, 1 year retention):
- Threads: 1000 × 5 MB = 5 GB
- Checkpoints: 1000 × 10 MB = 10 GB
- Vectors: 100K vectors × 1536 dims × 4 bytes = 600 MB
Recommendation: 100 GB PostgreSQL storage, expandable
Scaling Triggers
When to Scale Up
Indicators:
- CPU utilization > 70% sustained
- Memory utilization > 80%
- PostgreSQL connection pool exhausted
- Thread queue depth > 20
- LLM rate limit errors frequent
Actions:
- Add daemon nodes (+1-2)
- Increase PostgreSQL replicas
- Tune pool sizes
- Optimize queries
When to Scale Down
Indicators:
- CPU utilization < 30% sustained
- Memory utilization < 40%
- Thread queue depth < 5
- Low concurrent thread count
Actions:
- Remove daemon nodes (-1)
- Reduce pool sizes
- Consolidate PostgreSQL replicas
Related Documentation
- Production Setup - Single-node deployment
- Monitoring Guide - Performance monitoring
- Security Hardening - Secure scaling
- Backup Recovery - Database backup strategies
Need scaling help? See Troubleshooting or calculate capacity with formulas above.