Scaling
Ragz is a modular monolith: one FastAPI codebase serving both the synchronous API and the Celery workers. That means scaling is mostly about giving the right process more of the right resource — there's no service mesh to reason about.
The levers
| Symptom | Lever |
|---|---|
Ingestion queue backs up (documents stuck in queued/processing) | Add Celery workers |
| Chat feels slow under concurrent users | Add uvicorn worker processes |
| OCR-heavy corpora (scanned PDFs) stall the ingestion queue | Dedicated ingestion worker pool, separate from interactive |
| Corpus keeps growing | More disk — Postgres, Qdrant, and MinIO volumes all grow with document count |
| You need local/air-gapped LLM inference | GPU tier — a host with a 16 GB+ GPU running Ollama/vLLM |
| Everything is fine except one org's spiky usage | Per-org/per-user quotas (modules/quotas), not more hardware |
Corpus size → disk
Every document lands in three places: MinIO (original file + extracted blocks), Postgres (metadata, chunks references, versions), and Qdrant (embeddings). Budget disk against the corpus, not against traffic — a 100-page-PDF-heavy workspace with light chat traffic needs more disk than a high-traffic workspace with a small corpus. The reference sizing in the README suggests 80–100 GB SSD for a typical single-node install (≈25–30 GB fixed for images and model weights, the rest grows with your corpus).
Concurrent users → RAM / vCPU
Two independent process pools consume CPU under load:
- uvicorn (the FastAPI process) — scales with concurrent chat/API requests. Each streaming chat response holds a connection open for the duration of the answer.
- Celery workers — scale with ingestion throughput (parsing, chunking, embedding, upserting to Qdrant).
They don't compete for the same resource pool if you run them as separate processes/containers, which is the compose default — so you can scale one without touching the other.
Adding Celery workers
Run more worker processes against the same queues. On Linux, use the
prefork pool (the Celery default) to get real multi-core parallelism per
worker process:
# From backend/ — add a second worker process on the interactive + default queues
uv run celery -A ragz.worker.celery_app:celery_app worker \
-Q interactive,default -l info --concurrency=4Each additional celery worker invocation is an independent consumer on the
same Redis-backed queues — start as many as your host (or additional hosts)
can support. On macOS, add --pool=solo for local dev; it doesn't parallelize
within a single worker, so for real concurrency run multiple --pool=solo
processes instead of relying on one.
Dedicated ingestion workers
If OCR (Docling on scanned PDFs) or large-batch ingestion is competing with interactive chat latency, split them onto separate queues and give OCR its own worker pool:
uv run celery -A ragz.worker.celery_app:celery_app worker \
-Q ingestion -l info --concurrency=2Route heavy ingestion tasks to the ingestion queue and keep interactive
responsive for chat-blocking work.
Adding uvicorn processes
Run multiple uvicorn workers (or multiple container replicas) behind your reverse proxy, load-balanced across them:
uv run uvicorn --factory ragz.api.app:create_app --workers 4 --port 8000Session state lives in Postgres/Redis, not in-process, so any worker can serve any request — scale horizontally without sticky sessions.
The GPU tier
Local/air-gapped LLM inference (Ollama or vLLM) needs its own host with a
16 GB+ GPU — this is a separate concern from the CPU-bound API/worker
scaling above. If you're also running local embeddings and rerank (the
tei/tei-rerank services), those can share the CPU tier or move to GPU
depending on throughput needs; see the model-placement table in the
README for the RAM cost of each
placement choice.
When to stop scaling a single node
Once a single host's uvicorn + Celery scaling isn't enough — sustained high concurrent chat load alongside heavy ingestion — the next step is running the API and workers as separate deployable units (still the same codebase, per the modular-monolith architecture) across multiple hosts, with Postgres moved to a managed instance as described in Production.