Anatomy of a Production RAG System

10 min read Module 1 of 9 Topic 2 of 25

What you'll learn

  • Draw the three planes of a production RAG system: ingestion, serving, and control
  • Explain why ingestion and serving must be independently scalable and deployable
  • Identify which components sit on the request's critical path and therefore own the latency budget
  • Design the index as a versioned contract that both planes read and write against
Building this at your company? For enterprise and company teams taking this to production: book a 30-minute session with our AI engineers for architecture guidance, code review, and a rollout plan for your use case.
Book a Team Session

Ask five vendors what RAG is and you’ll get five diagrams with an arrow from “your data” to “magic” to “answer.” Before we build anything, let’s replace that with the actual reference architecture that production teams converged on by 2026. Every later lesson in this course slots into one box of this picture, and when you’re debugging a system six months from now, this diagram is what you’ll mentally walk through to localize the problem.

The three planes

A production RAG system is really three loosely coupled systems wearing one name:

  1. The ingestion plane (offline). Connectors pull documents from sources, parsers turn them into clean text and structure, chunkers split them, embedders vectorize them, and writers upsert into the indexes. It runs on queues and workers, tolerates retries, and never blocks a user. Nothing here has a human waiting on the other end of an HTTP request.
  2. The serving plane (online). A user question comes in, gets rewritten and classified, hits the indexes through hybrid search with permission filters, candidates get reranked, the best chunks are assembled into a prompt, and the model generates a grounded, cited answer. Every millisecond here is felt by a human, and every stage is a candidate for the latency budget.
  3. The control plane. Evaluation suites, tracing, quality dashboards, user feedback, and drift detection. It touches no request directly, but it’s the difference between a system you operate with confidence and a system that quietly operates you, degrading for weeks before anyone notices.
flowchart TB
    subgraph OFF["Ingestion plane (async)"]
        SRC["Sources\nDrive, Confluence, Slack, tickets"] --> CONN["Connectors +\nchange detection"]
        CONN --> PARSE["Parsing\nlayout-aware, VLM OCR"]
        PARSE --> CHUNK["Chunking +\ncontextual enrichment"]
        CHUNK --> EMB["Embedding\n+ metadata, ACLs"]
        EMB --> IDX[("Indexes\nvector + lexical")]
    end

    subgraph ON["Serving plane (request path)"]
        Q["User question"] --> QI["Query intelligence\nrewrite, filters, route"]
        QI --> RET["Hybrid retrieval\nwith ACL filter"]
        RET --> RR["Reranker"]
        RR --> GEN["Grounded generation\nwith citations"]
        GEN --> A["Answer + sources"]
    end

    IDX --> RET

    subgraph CTL["Control plane"]
        EV["Evaluation suite"] -.-> RET
        EV -.-> GEN
        TR["Tracing + dashboards"] -.-> ON
        FB["User feedback"] -.-> EV
    end

    style SRC fill:#EEF0F7,stroke:#6366F1,color:#0F172A
    style CONN fill:#EEF0F7,stroke:#6366F1,color:#0F172A
    style PARSE fill:#EEF0F7,stroke:#6366F1,color:#0F172A
    style CHUNK fill:#EEF0F7,stroke:#6366F1,color:#0F172A
    style EMB fill:#EEF0F7,stroke:#6366F1,color:#0F172A
    style IDX fill:#f0fdf9,stroke:#0D9488,color:#0F172A
    style Q fill:#EEF0F7,stroke:#6366F1,color:#0F172A
    style QI fill:#f0fdf9,stroke:#0D9488,color:#0F172A
    style RET fill:#f0fdf9,stroke:#0D9488,color:#0F172A
    style RR fill:#f0fdf9,stroke:#0D9488,color:#0F172A
    style GEN fill:#fff7ed,stroke:#f59e0b,color:#0F172A
    style A fill:#f0fdf9,stroke:#0D9488,color:#0F172A
    style EV fill:#fff7ed,stroke:#f59e0b,color:#0F172A
    style TR fill:#fff7ed,stroke:#f59e0b,color:#0F172A
    style FB fill:#fff7ed,stroke:#f59e0b,color:#0F172A

Why the planes must stay separate

The ingestion plane is bursty and slow, by nature rather than by poor engineering. A Confluence migration might dump 80,000 pages on you in an hour; a single scanned PDF might take 40 seconds in a VLM parser; a bulk permission change might require touching every chunk of a large space. The serving plane is the opposite: steady, high-frequency traffic with a hard latency budget of a few seconds end to end, where a single slow request is a visible incident, not a background job you can queue and forget.

Couple them and each inherits the other’s worst property. Parse at question time and users wait 40 seconds for a scanned PDF to become text before they even get an answer. Serve queries from your ingestion workers’ database and a reindex operation contends for the same resources your live users need, taking your assistant down during the exact window you’re trying to improve it. Keep them separate and each plane scales along the dimension that actually matters to it: ingestion scales in worker count during a migration and idles the rest of the time, serving scales in query replicas during business hours and can shrink overnight. They also deploy independently: a new chunking strategy rolls out by reindexing in the background and cutting over atomically, never by redeploying the customer-facing API and risking a live outage over a data pipeline change.

The one place the two planes meet is the index, and it deserves to be treated with the same rigor as a database schema, because that’s functionally what it is. Every chunk written by ingestion and read by serving carries an implicit contract: chunk text, source metadata, ACL tags, timestamps, and an embedding model version. Both planes must agree on that contract’s current shape. Change it (a new embedding model, a new chunking strategy, an added metadata field) and you’re making a schema migration, which is why Module 2’s incremental sync lesson spends real time on blue-green reindexing rather than treating index updates as a detail.

The serving path owns the latency budget

Here’s the skeleton of the serving path as code. The point isn’t the framework, it’s the shape: every stage is optional-on-failure except retrieval itself, and every stage’s timing is something you will eventually plot on a dashboard.

# serving.py - the request path in ~30 lines.
# Each stage degrades gracefully: if an enhancement fails,
# we log it and continue with what we have.

async def answer(question: str, user: User) -> Answer:
    # 1. Query intelligence (fast LLM call, ~150ms).
    #    On failure: use the raw question unchanged.
    plan = await safe(rewrite_and_plan, question, default=QueryPlan(question))

    # 2. Hybrid retrieval with the user's ACL filter.
    #    This is the one stage that must succeed; without
    #    candidates there is nothing to ground on.
    candidates = await retrieve(
        query=plan.rewritten,
        filters=plan.filters,
        acl=user.permission_tags,   # enforced IN the index query
        top_k=40,
    )

    # 3. Rerank 40 candidates down to the best 8.
    #    On failure: keep the fusion order, take the top 8.
    top = await safe(rerank, plan.rewritten, candidates, default=candidates[:8])

    # 4. Grounded generation with citations, streamed to the user.
    return await generate_grounded(question=question, chunks=top, user=user)


async def safe(fn, *args, default, timeout: float = 0.4):
    """Every enhancement stage runs through this wrapper.
    A slow or broken enhancement degrades the answer; it
    never takes the whole request down with it."""
    try:
        return await asyncio.wait_for(fn(*args), timeout=timeout)
    except Exception as e:
        log.warning("stage_degraded", stage=fn.__name__, error=str(e))
        return default

Notice what’s absent from this function: no parsing, no embedding of documents, no index writes. Those all happened hours ago on the ingestion plane, asynchronously, with nobody waiting. The only embedding computed at request time is the query’s own, which is why query embedding latency (typically 20 to 60ms for a hosted API) is worth tracking separately from retrieval latency in your traces.

The safe wrapper is doing more architectural work than its five lines suggest. It’s the mechanism that turns “reranker down” from an outage into a quality degradation, and it’s the same pattern every enhancement stage in this course will use: query rewriting (Module 5), filter extraction (Module 5), and reranking (Module 4) are all improvements over a working baseline, never single points of failure. The one exception, deliberately, is the retrieval call itself: there is no graceful degradation from “we have candidates” to “we have none,” so retrieval gets a real, generous timeout and genuine error handling rather than a silent default.

The control plane is not optional

Every team ships the first two planes; the business requirement to answer questions forces that much. The teams that succeed past the first few months also ship the third, and the gap between those two groups is almost entirely explained by what happens after launch, not by the initial build quality.

Without evaluation and tracing, quality regressions are invisible from the inside. You swap an embedding model because a benchmark looked good; retrieval recall drops 12 percent specifically on legal documents, because that collection’s vocabulary didn’t transfer well to the new model; and you find out three weeks later from an angry Slack thread, by which point dozens of users have quietly stopped trusting the tool and won’t come back even after you fix it. The control plane exists to catch that regression on day one, ideally before it ships.

We dedicate all of Module 8 to this plane, building a golden evaluation set, component-level metrics that isolate which stage broke, and drift detection that catches slow decay rather than sudden breaks. For now, one rule worth adopting from lesson one of your build: from day one, log every query, its rewritten form, its retrieved chunk IDs with scores, and the final answer with citations. That log costs almost nothing to start collecting, and it is the raw material every later evaluation technique in this course depends on. Teams that start logging on day 90 lose 90 days of real production failure cases they can never fully reconstruct.

Knowledge Check

4 questions to test your understanding

1 Your reranker goes down. In a well-architected system, what should happen to user queries?

2 Where does document parsing belong, and why?

3 Which of these belongs to the control plane rather than the data path?

4 A team deploys a new chunking strategy by editing the ingestion code and letting nightly jobs slowly re-process documents over the following week. Old and new chunking styles coexist in the index for days. What architectural property does this violate?

Discussion

Questions and notes from learners on this topic

Loading discussion…

Go further with expert guidance

Ready to build production AI?
Talk to our R&D team.

These courses give you the foundation. Our embedded AI teams take you from prototype to production in 30–90 days, with your team, your codebase, your goals. Book a free strategy call to see how we can accelerate your AI initiative.

30 minutes · No obligation · Expert AI engineers, not sales reps

AI Architecture Review

Audit your current stack and identify high-impact improvements

Project Review

Get expert feedback on your AI implementation and codebase

Team Mentoring

Upskill your engineers with hands-on AI coaching sessions

AI Strategy

Define your AI roadmap, prioritization, and implementation plan