AI Engineering Playbook
System Design

Design: Document Q&A at Scale

High-volume document question-answering with freshness and access control.

Prerequisites

The problem

Design a document Q&A API that serves thousands of customers as a SaaS product — or, equivalently: your RAG demo works; make it serve 500 QPS across 2,000 tenants with a contractual SLA.

Where enterprise RAG is a quality-and-permissions problem at tiny QPS, this is the throughput and multi-tenancy variant. Isolation, cache design, tail latency, and what you shed under load matter more than "can we retrieve a good chunk in a demo."

In the governed enterprise platform, this is internal document Q&A turned into a multi-tenant product: many corpora, strict isolation, and a bill where the LLM API dwarfs infrastructure.

Why it is hard

Tenant size follows a power law — ~20 whales with millions of docs, a long tail with ~1k. Shared everything creates noisy neighbors; dedicated everything is impossible for the tail. A cross-tenant leak ends the company, so isolation must be structural at every layer (auth, store queries, caches), not a single WHERE tenant_id = ?.

Contractual p95 forces engineered tails: hedges, admission control, per-tenant quotas. A 5-minute upload SLA dies if a whale dumps 5M docs into a shared queue without fair ingest. The LLM bill is ~10:1 over infra, so caches and model routing are the product. Query mix varies by tenant (support-KB repeats; discovery does not), so global cache policy fails.

Alternatives that lose: one shared unpartitioned index; a global semantic cache (leak + wrong-corpus answers); scaling the LLM tier without caching (burn the margin).

Requirements and scale

Clarifying questions (each tied to a design consequence)

  1. Tenant size distribution? Power law → tiered isolation (dedicated shards for whales, shared partitions for the tail).
  2. SLA — latency, availability, freshness? Contractual p95 forces hedges and per-tenant quotas.
  3. Upload-to-queryable target? Minutes vs hours is a 10× difference in indexing architecture.
  4. Query mix: repeated or unique? Support-KB is caching gold; discovery is not. Cache per-tenant.
  5. Logical or physical isolation? Dedicated infra or region pinning is a product tier.
  6. Sync or async? Bulk QA over 10k docs should be an async batch path (~50% cost).

Requirements (assumed)

KindRequirement
Scale2,000 tenants, ~100M documents (top 20 hold ~50%)
Scale500 QPS peak, multi-region
Latencyp95 ≤ 2.5 s full answer; p95 generation TTFT ≤ 800 ms (~1.1 s end-to-end first token on miss path)
FreshnessUpload-to-queryable ≤ 5 min
Availability99.9%
IsolationStrict tenant isolation — cross-tenant leak is catastrophic

Capacity estimates

QuantityEstimateDerivation
Corpus~250B tokens → ~500M chunks100M × ~2.5k tokens; ~500 tokens/chunk
Vector storage~750 GB int8 (1536-dim)500M × ~1.5 KB; ~12–15 shards + replicas
Query load500 QPS peak ≈ 20M/day sustained60% interactive, 40% API/batch
LLM cost~$0.015/query → ~$300k/mo uncached4k in / 300 out at ~$3/$15 per M (illustrative mid-2026); after cache+routing ~$120–150k/mo
Ingest~1M docs/day, 10× burst~12 docs/s sustained; whale onboarding is the burst
In-flight (miss path)~800 concurrentLittle's law: 500 QPS × 1.6 s mean — size pools for this

Cache and routing are the unit-economics design: LLM spend dominates infra ~10:1.

The design

Multi-tenant isolation — three tiers, one code path

Tiered multitenancy pools small tenants for cost, silos large ones for noise isolation, and sells full physical isolation as a premium tier. Modern vector stores implement this directly — payload partitions for the long tail, named shards for whales, transparent tenant promotion from a shared fallback shard (Qdrant multitenancy, scaling and operations).

  • Tail (~1,900): one collection per embedding model, partitioned by tenant_id with a mandatory filter. Thousands of collections is an anti-pattern. Index the tenant field as a tenant keyword (is_tenant-style) so vectors co-locate.
  • Whales (~20): dedicated shards. Promote when corpus size or sustained QPS crosses a share-of-pool threshold — a hot small tenant is still a noisy neighbor.
  • Contractual isolation: dedicated cluster/region, priced as such.

Enforcement is structural: tenant_id from the verified JWT only, injected by middleware; the repository refuses unfiltered queries; cache namespaces are tenant-scoped.

Common misconception

Filtering by tenant_id in the vector query is necessary but not sufficient. The classic leak is an unscoped semantic cache serving tenant A's answer to tenant B's similar question.

Freshness pipeline

Upload → durable queue → parse/chunk/embed → shard upsert → cache invalidation for that tenant. Weighted fair queuing so a whale's 5M-doc backfill cannot blow the 5-min SLA for a tail tenant's single upload. Track upload-to-queryable as a per-tenant SLI. Deletes and permission changes take a fast lane (< 60 s). Chunking: ingestion and chunking.

Caching — four layers, cheapest first

LayerHit rate (est.)SavesKey design point
L1 exact response10–20% (support-KB higher)full pipelinekey = hash(tenant, normalized query, corpus version)
L2 semantic response+5–10%full pipelineper-tenant only; similarity ≥ 0.97 + safety check
Provider prompt cache60–90% of stable prefix tokensup to ~90% of prefix input coststatic system + tenant policy first; query and chunks last
Embedding cache20–40% of query embeds~20 ms + embed costtrivial, do it

A semantic cache keys answers by embedding similarity rather than exact string match. That raises hit rate on rephrases — and makes tenant scoping non-negotiable. Global caching is legitimate only for tenant-agnostic artifacts (common query embeddings), never for answers.

Generation and model routing

A cheap classifier (or heuristics: query length, retrieval score margin) routes ~60% of high-confidence factual lookups to a small model (illustrative ~$0.3/$2.5 per M); the rest to mid-tier. Router mistakes are caught by eval. Batch endpoints use provider batch APIs at roughly half price. Call the LLM gateway for failover, TPM, and cost attribution (choosing models).

Serving path — latency budget (cache-miss)

Stagep50p95Degradation lever
Admission + auth + cache probes8 ms20 ms
Query embed15 ms40 mscache
Hybrid retrieval (tenant-filtered)40 ms120 msshrink k, drop BM25 leg
Rerank 60 → 850 ms110 msfirst thing shed under load
Generation TTFT350 ms800 msroute to small model
Generation total (300 tok streamed)1.2 s2.2 scap max_tokens
End-to-end~1.6 s≤ 2.5 s~10% headroom

p95 pre-generation ≈ 290 ms + gen TTFT 800 ms1.1 s end-to-end to first token on the miss path. The 800 ms number is generation TTFT; hit tighter end-to-end first token via cache hits or by shedding rerank. Tail control: hedge retrieval at a p90 timer; per-tenant concurrency caps; timeout per stage with its degradation lever.

Evaluation

Per-tenant quality floors — a global average hides one broken corpus. Nightly canaries track recall, faithfulness, and latency; alert on per-tenant deltas. Sample semantic-cache hits and tighten the threshold if false-hit rate exceeds ~1%. Online: thumbs ratio, $/query per tenant. See RAG evaluation and evals and testing.

Key decisions and tradeoffs

DecisionChoiceTradeoff
IsolationTiered: shared filtered / dedicated shard / dedicated clusterPromotion complexity; correct economics
Semantic cachePer-tenant onlyLower global hit rate; avoids leak and wrong-corpus answers
Rerank under loadFirst shedSlight quality dip; protects TTFT
Model routing~60% small modelRouter errors need eval catch; large cost/latency win
Ingest fairnessWeighted fair queue per tenantWhale onboarding slower; protects everyone else's SLA
Batch pathAsync + ~50% batch discountProduct complexity; keeps interactive path clean
Prompt cache layoutStatic prefix firstPrompt discipline; largest cheap cost lever

Key insight

At 500 QPS the design centers on isolation + tails + COGS. A perfect chunker that ignores tenant-scoped caches and fair ingest still produces incidents finance and security care about more than recall@10.

The flows

Flow A — Interactive query (cache miss)

Admission (per-tenant rate + interactive priority) → L1 then L2 (tenant-scoped) → on miss: embed → tenant-filtered hybrid retrieve → optional rerank → model router → generate → stream, trace, fill caches with tenant keys + corpus version.

Breaks when: tenant filter omitted, cache key drops tenant, or rerank saturates without shed.

Flow B — Interactive query (cache hit)

Same admission. L1 returns the full answer, or L2 after similarity ≥ 0.97 + safety check. Audit-log the hit for attribution.

Breaks when: ingest failed to invalidate after an update, or L2 false-positive.

Flow C — Document upload to queryable

Upload → fair-queued per tenant → parse/chunk/embed → upsert to correct shard tier → invalidate that tenant's cache entries. SLI: ≤ 5 min queryable; deletes/permission changes < 60 s fast lane.

Breaks when: a whale monopolizes the queue, or invalidation is best-effort.

Flow D — Overload degradation

Detect stage latency, provider errors, queue depth. Shed in order: rerank → force small model → relaxed cache threshold with "may be outdated" flag → retrieval-only (passages + citations) → admit paid interactive last, reject batch/free first. Two providers behind the gateway; monthly failover drills.

Breaks when: degradation is ad hoc, or batch is not distinguishable from interactive.

Failure modes and degradation

The ladder in Flow D is the production response — each step trades quality for staying up, including retrieval-only mode (good search, not a 503, under total LLM outage). Bad chunker deploys recover from the document store via shard-scoped reindex, blue/green. Align with reliability and LLM gateway.

Common drill-downs

Prove tenant A never sees tenant B's data. JWT-derived tenant_id (never client-supplied); repository that cannot express unfiltered queries; tenant-scoped caches; adversarial cross-tenant tests; periodic auditor sampling vector payloads against ownership. Design the cache leak first.

p95 is 2.5 s but p99 is 9 s. Decompose by stage: generation-length outliers (cap max_tokens, measure TTFT separately); ANN on the biggest shared shard (promote the whale); cold cache after deploys (warm L1 from top-1k per tenant); provider TTFT spikes (hedge high-tier tenants).

A tenant uploads 5M docs at 9 am. Without fairness every tenant misses the 5-min SLA. With it: fair queuing, autoscaled workers, async bulk-import with progress, and promotion to a dedicated shard before shared-tier search latency bloats.

Why per-tenant semantic cache? Same question, different corpus, different correct answer. A global answer cache is both a leak and a correctness bug.

Where does the money go? ~10:1 LLM vs infra. Levers in order: prompt-prefix caching → small-model routing (~60%) → response caches → only then infra tuning.

Test yourself

You find a semantic cache implementation keyed only by embedding(query). What is wrong, and how bad is it?

500 QPS peak, mean latency 1.6 s. Roughly how many in-flight requests?

Whale promotion threshold is 1M docs. A tenant has 200k docs but 100 QPS. Promote or not?

After a chunker deploy, one tenant's faithfulness collapses; global average barely moves. What was missing?

Prompt caching saves ~90% of prefix input cost. How do you structure the prompt so it hits?

Go deeper

Where this connects

  • Enterprise RAG — quality/ACL-first at low QPS; contrast what changes when multi-tenancy and 500 QPS dominate.
  • LLM gateway — provider failover, TPM limits, and cost attribution this path should not reimplement per product.
  • Filtering and metadata — pre-filter mechanics tenant partitions rely on.
  • Reliability — degradation ladders and timeout patterns in general form.
Design: Enterprise RAG

On this page