Skip to main content

Capacity & scaling

Current reality​

Ratiba M13 is a single-VPS Docker Compose stack serving a 5-10 trusted-customer beta cohort. There has been no load test. The numbers below are engineering estimates derived from the architecture decisions — not measurements from production traffic.

Where load concentrates​

The yellow nodes are the first things that would bottleneck under concurrent load. The pink node is the primary cost surface.

Known concentration points​

1. Redis SETNX booking mutex (per ADR-0003 D3)

Each active booking thread holds a Redis lock (ratiba:fsm:lock:<thread_id>, 30s TTL with exponential-backoff retries to a 10s ceiling). This is per-thread, not per-tenant — two concurrent bookings for the same tenant each hold their own lock. In M13 with <10 active tenants and light traffic, lock contention is not a concern. At higher concurrency the retry backoff adds latency; the ceiling keeps it bounded.

2. Shared asyncpg pool (per ADR-0002 D4)

One process-wide pool of DB_POOL_SIZE=20 connections (env-tunable). Every booking FSM turn that touches the database borrows from this pool. The pool is shared across all tenants; a spike on one tenant competes with all others. At 5-10 beta tenants with infrequent concurrent bookings, 20 connections is generous. The practical limit before contention becomes visible is roughly DB_POOL_SIZE × avg_turn_duration in QPS — not measured yet.

3. Per-tenant psycopg micro-pools (per ADR-0002 D4)

The LangGraph TenantScopedSaver uses a dedicated 1-2 connection pool per tenant, created lazily and closed after 30 min of inactivity. At M13's beta scale this is fine. The connection math becomes relevant at ~100 active tenants (2 idle connections each = 200 open Postgres connections against a tuned-VPS limit of 500-1000). This is documented explicitly in ADR-0002 as the trigger for adding PgBouncer in transaction-pooling mode.

4. The public.* tenant registry (per ADR-0002 D1)

Three tables in the shared public schema (tenants, tenant_admins, payment_routing) are read on every inbound message at the channel boundary. At M13's tenant count these are trivially small. PostgreSQL's schema catalog adds overhead per schema — this becomes relevant around ~10,000 tenants per instance, a scale we are not designing for now.

5. Per-booking LLM cost ceiling (per ADR-0005 D4)

Every booking consumes LLM tokens at $0.05 soft / $0.20 hard per-booking defaults (per-tenant configurable). The cost ceiling is the primary capacity-management lever for M13: it bounds runaway spend on any single booking thread and escalates to the admin rather than burning uncapped tokens. This is a financial safety rail, not a performance one — it does not prevent high-frequency bookings from accumulating cost at scale.

6. Daily nightly reaper (per ADR-0007 D5)

A single cron job at 3 AM EAT runs public.payment_routing expiry, per-tenant checkpoints_archive moves, and handoff_log_archive moves. In M13 this runs as a Docker worker container. It has not been tested under load; if the reaper falls behind at scale, public.payment_routing and the checkpoint tables grow unbounded until the next run.

Practical limits (M13 estimates, unverified)​

ResourceM13 defaultFirst constraint appears when…
DB connections (asyncpg)20 shared> 20 concurrent FSM turns holding a DB connection
DB connections (psycopg micro-pools)1-2 per tenant~100 active tenants (approach VPS connection limit)
Redis memorysingle instance, no cap configuredLarge checkpoint blobs accumulate; check redis-cli INFO memory
LLM cost per booking$0.05 soft / $0.20 hardAny individual booking that loops the FSM excessively
Tenant registry tablestrivially small~10,000 tenants / Postgres instance (catalog overhead)

We have not load-tested any of these. The table represents design-time estimates from the ADR authors. Real numbers from the M13 beta cohort will calibrate these before M14.


Target posture​

The scaling levers below are aspirational — post-pilot targets, not M13 commitments. None are in scope until the beta demonstrates sustained load that warrants them.

Database​

  • Read replica for reporting and analytics queries (/admin/analytics, Langfuse trace queries, catalog listing). The write path stays on the primary.
  • PgBouncer in transaction-pooling mode in front of Postgres once active-tenant count exceeds ~100. ADR-0002 documents this as the known trigger point for the per-tenant micro-pool connection math.
  • Per-tenant Alembic invocations remain independent of this — scripts/migrate-all-tenants.sh already handles bulk upgrades per ADR-0002 D3.

Redis​

  • Redis Sentinel (primary + 2 replicas) for HA once the booking mutex and FSM checkpoints are proven load-bearing at scale. Sentinel is simpler than Cluster for Ratiba's access patterns (key-per-thread, no hash-slot distribution needed).
  • Per-tenant key prefix scheme (already in ADR-0003) survives unchanged under Sentinel.

Backend​

  • Horizontal backend scaling behind a load balancer (nginx or Cloudflare Load Balancer). The current_tenant contextvar and current_booking_cost contextvar (ADR-0005 D4) are both set per-request at the channel boundary — there is no process-level tenant state, so horizontal scaling is architecturally clean.
  • Worker horizontal scaling: the ARQ worker is a separate container running the same image with an overridden CMD. Additional worker replicas can be added to the compose stack without code changes.

LLM cost​

  • Per-tenant cost_ceiling_soft_usd / cost_ceiling_hard_usd columns on public.tenants (ADR-0005 D4) are tunable via UPDATE public.tenants. Calibrate from observed booking cost distribution after the first 100 production bookings per tenant.
  • LLM provider failover: the LLMRouter (ADR-0005 D5) is a YAML-config swap; adding a cheaper provider or a Tier-2 fallback is a role_assignments.yaml change, not a code change.

Voice / LiveKit​

  • LiveKit moves to a dedicated server or LiveKit Cloud once voice traffic justifies it. In M13, LiveKit runs in the same compose stack on the same VPS host. The Q7 lock defers multi-tenant LiveKit production deployment to M14.

Monitoring graduation​

  • Lean observability (docker logs + daily WhatsApp digest) is right for M13. See the Observability runbook for the current posture and the post-pilot instrumentation targets (Prometheus + Grafana + alerting).