smart-chatbot-api / docs /scaling_reference.md
GitHub Actions
Deploy from GitHub Actions (2026-05-31 09:04 UTC)
55c0d78
|
Raw
History Blame Contribute Delete
4.75 kB

Scaling Reference

Purpose: What to know and what to change when traffic grows. Written at V1 (sync, low-traffic SME demo). Read this before making any scaling decision.


Current Architecture (V1 Baseline)

Layer Technology Limit
Web framework FastAPI (sync, thread pool) ~200–500 concurrent requests before thread exhaustion
DB driver psycopg2 (sync) Fine up to ~100 concurrent DB queries
DB PostgreSQL (single instance) Fine up to ~500–1000 connections with PgBouncer
LLM Groq (sync HTTP call, 1–3s) Real bottleneck β€” async won't help here
Vector DB ChromaDB (in-process) Fine for low-volume; becomes a bottleneck at scale
Deployment Single Docker container No horizontal scaling

The Real Bottleneck: LLM Latency

Every /chat request waits 1–3 seconds for the LLM response. At V1 traffic (5–50 concurrent users), this is fine. At scale:

  • Async DB saves ~5ms per request
  • LLM call costs 1,000–3,000ms per request

Conclusion: Optimizing the DB layer before solving LLM latency is premature. Address LLM first.


Scale Thresholds and What to Do

< 100 concurrent users β€” Do nothing

Current sync architecture handles this comfortably. Focus on product.

100–500 concurrent users

  1. Add PgBouncer β€” connection pooling in front of PostgreSQL. Prevents DB connection exhaustion. Config change only, no code change.
  2. Add Redis caching β€” cache system prompts and KB entries per tenant. Eliminates redundant DB reads on every /chat call.
  3. Horizontal scale β€” run 2–3 replicas behind a load balancer (Nginx/Traefik). Stateless FastAPI app supports this with zero code changes.

500–5,000 concurrent users

  1. LLM request queue β€” move LLM calls to a background worker (Celery + Redis). /chat returns a job ID immediately; client polls for the result. Eliminates thread exhaustion from long-running LLM calls.
  2. ChromaDB β†’ managed vector DB β€” replace in-process ChromaDB with Qdrant or Weaviate (dedicated service). In-process ChromaDB doesn't scale horizontally.
  3. Read replicas β€” add PostgreSQL read replicas for conversation history and KB reads.

> 5,000 concurrent users

  1. Async migration β€” switch to asyncpg driver + async SQLAlchemy + async FastAPI. Requires rewriting all get_db dependencies, session management, and test fixtures. High cost β€” only justified at this scale.
  2. Kubernetes β€” horizontal pod autoscaling, per-tenant rate limiting at ingress, proper resource limits.
  3. LLM provider strategy β€” provider failover, per-model rate limits, tenant-level concurrency caps.

Async Migration Path (when justified)

When async is finally needed, the migration order is:

  1. Change DATABASE_URL scheme: postgresql:// β†’ postgresql+asyncpg://
  2. Replace SessionLocal with AsyncSession in database.py
  3. Replace get_db dependency with async generator
  4. Change all router functions from def to async def
  5. Replace all db.scalars(...) with await db.scalars(...)
  6. Rewrite all test fixtures in conftest.py (async session, async client)
  7. Run full test suite β€” expect failures in every file that touches the DB

Cost: ~2–3 days of pure refactoring with no new features. Only do this when load data proves it's needed.


One-Way Doors to Avoid Before Scale

These decisions become very expensive to undo at scale β€” make them correctly from the start:

Decision Risk if wrong
No credit_events table Can't reconstruct billing history β€” dispute with no evidence
No tenant isolation in ChromaDB Data leakage between tenants β€” legal liability
No API versioning (/api/v1/) Can never change request/response contract without breaking all clients
No llm_models versioned rates Can't prove which rate applied to a historical charge

Summary

Sync is fine for V1 and beyond. The LLM call is the bottleneck, not the DB driver. Add PgBouncer + Redis + horizontal replicas before ever touching async. Migrate to async only when data shows you need it β€” and budget 2–3 days for the rewrite.