Skip to main content
InMotion Cloud
BLOG · AI

Surging RAG: Processing 17,000 Pages in 20 Minutes (Full Technical Report)

Full engineering report on scaling a self-hosted RAG pipeline to 17,001 pages in under 20 minutes on InMotion Cloud with Fixed Cost Advantage.

Updated October 22, 2026 by Suraj Bangade

24 Minutes to Read

Self-hosted RAG surge graphic showing document ingestion scaling across worker nodes to meet a 20-minute deadline.

1. Executive Summary

We recently built a production-grade AI document assistant on InMotion Cloud, then pushed it to its absolute limits with an aggressive real-world deadline. This is the full report; if you want a shorter version, refer to the 7-minute overview.

  • The Challenge: A customer uploads a 17,001-page archive and expects full searchability within 20 minutes.
  • The Bottleneck: Three baseline OCR workers fell severely behind schedule.
  • Autonomous Scaling: A lightweight watcher detected the delay early and queried the InMotion Cloud OpenStack API to auto-provision virtual machines up to a 9-worker cap.
  • Rapid Onboarding: New instances joined the Kubernetes cluster and began processing pages roughly 90 seconds after creation.
  • Success: All pages were fully indexed approximately one minute ahead of the 20-minute deadline.
  • Accurate RAG Output: Post-ingestion, the assistant provided exact answers complete with verifiable PubMed Central citations.

During the burst, the Fixed Cost usage metric climbed from $169.53 to $430.56 (~8% over the $399 plan allowance), shifting platform health status from Good to Bursting. Despite exceeding plan limits, the monthly bill remained fixed at $399 with zero overages, throttling, or capped performance.

Our architecture prioritizes privacy by keeping documents strictly within the tenant boundary:

  • Reading pages (OCR) and turning text into searchable vectors (embedding) both run on our own VMs.
  • The documents, page images and vectors never leave the project.
  • Only one thing leaves: when someone asks a question, the question plus the few passages relevant to it go to a hosted language model to write the answer.
  • That last step can also be replaced with a model running inside the project.

2. The scenario

This setup addresses a common challenge in engineering Enterprise Retrieval-Augmented Generation (RAG) platforms.

  • The customer: a company that has a large back-catalogue of documents (papers, scans, reports) and wants an assistant that can answer questions from them.
  • The promise: "upload your archive and it will be searchable within 20 minutes."
  • The archive: almost 17,000 pages. About 70% are scanned pages or images that must go through OCR (optical character recognition). The rest are PDFs, Word files and plain text.
  • The problem: OCR is slow and CPU-hungry. Each worker reads about two pages per second at 85–90% CPU. Three workers would need about 47 minutes, more than twice the 20-minute promise.
  • The answer: add machines. Not by hand, and not after the fact. A watcher projects the finish time continuously, and as soon as it sees the promise will be missed, it adds VMs.
  • The cost question: adding six large VMs pushes usage well past the plan's allowance. On a pay-as-you-go cloud you pay for those machines by the hour for as long as you keep them. On InMotion Cloud's Fixed Cost Advantage plan it shows up as Bursting and the fee does not change.

Each run is checked with a control question whose answer exists only in the new archive:

"In critically ill older patients starting enteral feeding, in what share of patients was serum asprosin high on the first day?"

Before the upload, the assistant replies "I don't have anything on that." After ingestion, the same question gets the right answer with a citation (96%, [PMC10000515]). This before-and-after proves that the new pages really were read and indexed.


3. Goals and ground rules

3.1 What we set out to prove

  1. A real, working RAG assistant, not a mock-up.
  2. A real surge that genuinely needs more machines, caused by real work, not by idle padding.
  3. The system adding VMs on its own, including the real wait while each new machine boots and joins.
  4. The Fixed Cost usage value crossing the plan total, the banner changing, and the fee staying flat.
  5. The added machines left running afterwards. On a flat-fee plan, you don't have to give capacity back while you decide whether the surge is permanent.

3.2 Ground rules

The results are meant to be checked by engineers, so the project followed these rules:

  • Every number is measured on live infrastructure and traceable to a log or result file.
  • The surge is simulated. We started the upload ourselves. No customer traffic was involved.
  • The archive is a public corpus, rendered into scanned pages so OCR has real work to do.
  • No tuning to manufacture machines. The scan quality, file mix and archive size were declared up front and never adjusted to make the system need more VMs. If measurement had shown the surge needed very few machines, we would have shrunk the scenario or dropped it.
  • The Fixed Cost figure is a usage value, not a bill. It values the resources allocated right now. How busy the CPUs are plays no part in it.
  • A short spike is not where the benefit lies. A 20-minute spike is cheap on any cloud. The real benefit is being able to keep the extra machines for days or weeks at the same fee.

4. Architecture at a glance

Surging RAG architecture diagram showing the operator workstation, InMotion Cloud OpenStack project on private network 10.20.0.0/24 with surge-bastion, k3s cluster, surge-vector-db, surge-ocr-worker-01 to 09, surge-assistant, OpenStack Nova API, Open WebUI chat, and Hosted LLM.

4.1 The machines

The cloud project holds five standing machines and up to nine workers. All of them sit on one private network.

MachineSizeJob
surge-bastionm7i.medium (2 vCPU / 4 GB / 100 GB)The only machine with a public IP and the only way in by SSH. Never part of the cluster.
surge-k8s-controlm7i.mediumk3s server (Kubernetes control plane).
surge-vector-dbm7i.mediumPostgres 17 with pgvector. Holds the work queue, page bytes and vectors.
surge-cluster-sparem7i.mediumSpare cluster node (an image registry was tried here and rejected).
surge-assistantm7i.mediumHermes Agent, the retrieval tool and the model adapter.
surge-ocr-worker-01…09m7i.2xlarge (8 vCPU / 32 GB / 500 GB)OCR and embedding. Workers 1–3 are always on; 4–9 are added during the surge.

4.2 Two flows

  • Ingestion (the heavy part): pages are uploaded into the database queue. Workers claim pages, OCR them, split the text into chunks, embed the chunks into vectors and write them back to the database.
  • Answering (the light part): a question arrives. The assistant searches the vectors, reranks the best passages and sends the question plus those passages to a language model, which writes the cited answer.

5. How we built it

This section describes each part and why it is built that way. It contains no code.

5.1 The document archive

  • Source: the PubMed Central (PMC) Open Access subset, limited to papers whose licences allow commercial use (CC BY, CC0, CC BY-ND). We fetched about 2,600 papers and recorded the provenance of each in a corpus manifest.
    • Real, licensed documents mean the assistant's answers can be checked by anyone on PMC.
  • Turning papers into an archive: a generator renders each paper as pages in a declared mix of formats:
FormatShare
Scanned PDF55%
Scanned TIFF15%
Native PDF (text layer)20%
Word document7%
Plain text3%
  • Scan quality: pages are rendered with a real 30-pixel TrueType font plus a fixed, declared amount of scan degradation. OCR recovers about 97.7% of characters and 95.7% of words. That is realistic, not perfect.
  • Size: 17,001 pages, 14.8 GB. The uncompressed TIFF scans (about 3.6 MB each) are 65% of the bytes.
  • Control page: the archive is guaranteed to include the paper behind the control question (PMC10000515), and the database is checked before every run to confirm that paper is not already indexed.

5.2 Upload and the shared work queue

  • Pages are uploaded straight into Postgres, which serves as both the queue and the storage for page bytes. Large binary columns are compressed with lz4.
  • Uploads are batched and each batch is flushed at 8 MB, so a run of big TIFFs can't exhaust database memory.
  • Upload speed: about 35.8 MB/s, 41 pages per second. Uploading the full archive took about 7 minutes, so pages arrive much faster than three workers can read them. That gap is the surge.
  • Why the queue is in the database: an earlier design kept queues on each node. New machines then joined with nothing to do. With one shared queue, a new worker starts pulling pages the moment it is ready.
  • How workers share work without collisions: each worker claims a page with a row lock that skips pages other workers already hold. Each claim has a lease, so a page held by a crashed worker returns to the queue. Chunk IDs are deterministic, so re-processing a page can't create duplicates.

5.3 OCR and embedding workers

  • Each worker VM runs one ingestion pod (a Kubernetes DaemonSet, so every new worker node automatically gets one).
  • The container image is built on Python 3.12 slim with:
    • Tesseract OCR with English language data, and Poppler for PDFs.
    • The bge-small-en-v1.5 embedding model, baked into the image so nothing is downloaded at runtime.
    • Pinned dependency versions and a non-root user.
  • Each pod runs 8 OCR processes, one per vCPU, each limited to a single thread. Without that limit, all eight processes fight over the same cores and throughput falls 148-fold.
  • Measured throughput: about 2.0 pages per second per worker inside the cluster, at 85–90% CPU. OCR is the bottleneck. Embedding has spare capacity (it needs about 7 of its 21 chunks per second).
  • Workers are tainted so only ingestion runs on them. The assistant and the database can never be scheduled onto a busy OCR node.

5.4 The vector database

  • Postgres 17 with pgvector runs on its own node, on persistent local storage (40 GB), with guaranteed CPU and memory so a busy cluster can't starve it.
  • It holds three things: the page queue, the page bytes and the text chunks with their vectors.
  • Each chunk keeps its source paper's PMC ID, so every answer can be cited back to a real paper.

5.5 The assistant

The request path for one question:

Assistant request and retrieval path showing User question to Hermes Agent, local bge-small embedding, pgvector similarity search, two-stage CPU reranking with TinyBERT and MiniLM-L-12, model adapter with groundedness guard, Hosted LLM, and cited answer or refusal.
  • Agent: the open-source Nous Research Hermes Agent (v0.21.5), exposed as an OpenAI-compatible chat API under the model name surge-support.
    • All 17 of its built-in toolsets are switched off, along with memory and session search. With web, memory or file tools on, the agent could answer the control question without the archive, or run commands on a production node.
    • Its only tool is our document search.
  • Retrieval tool: provided to Hermes over the Model Context Protocol (MCP) as a single tool, search_documents.
    • It embeds the question locally with the same bge-small model the workers use and runs a similarity search in pgvector.
    • The agent may search up to twice per question, for example once for each paper in a two-part question.
    • A two-stage reranker picks the best passages: the fast TinyBERT model narrows the field, then the more careful MiniLM-L-12 model judges what remains. Both run on CPU on the assistant machine.
    • Full passages are returned rather than snippets. With 400-character snippets, the model invented figures beside correct citations.
    • Passages below a relevance floor are dropped. If nothing relevant remains, the answer is the fixed refusal line.
  • Model adapter: a small OpenAI-compatible server between Hermes and the language model.
    • It drives the hosted model through the Cursor Python SDK's local runtime, with the SDK's own tools disabled. It translates tool calls using prompting, so the model can request searches.
    • It has a second, generic driver for any OpenAI-compatible endpoint. That driver is how a local model would plug in (see §9.4).
    • It runs a groundedness guard on every final answer: any number in the answer that does not appear in the conversation or the retrieved passages triggers one retry. If the retry still fails, the reply becomes the refusal line. We added this after the model once computed a wrong percentage and cited a real paper for it.
  • Model: claude-haiku-4-5. It made correct tool calls 10 times out of 10 in testing; the alternative we tried managed 9 out of 10.
  • Persona: a short instruction file tells the agent to answer only from retrieved passages, cite the PMC ID and use the exact refusal line when the archive has nothing.

5.6 The chat front end

  • The chat front end is Open WebUI, hosted on Nexus (our internal development platform), connected to Hermes as an "OpenAI API connection" and set up as a workspace model named surge-support.
  • Open WebUI reaches Hermes through a restricted two-hop SSH tunnel: Nexus → bastion → assistant machine.
    • Each tunnel key may forward to exactly one address and port, and may not open a shell.
    • Host keys are pinned, and Hermes still requires its own API key.
  • The assistant machine has no public address. All outside traffic passes through the bastion.

5.7 Dashboards

Operators follow a run on these dashboards, all fed by live data from the cluster, the database and the OpenStack API:

DashboardWhat it shows
Fixed Cost viewThe live usage value against the $399 plan, the banner, and one ring per resource (vCPU, RAM, local disk) showing allocation against its allowance. Fed from the OpenStack server list.
BacklogPages waiting, drain rate, projected finish and the 20-minute deadline as a line. Projections count machines that are still booting.
PipelinePage counts at each stage, with measured rates between stages. The hosted model is drawn outside the flow.
Node readinessEach worker moving through Booting, Joined and Reading.
kube-ops-viewKubernetes nodes and pods appearing.
Worker CPU monitorPer-worker CPU use, to confirm the workers are genuinely busy. Kept separate from the usage value, which does not depend on CPU use.

6. Wiring InMotion Cloud (OpenStack) into the project

InMotion Cloud is built on OpenStack, so the project uses the standard OpenStack APIs: Keystone for authentication, Nova for compute and Neutron for networking. Horizon and InMotion Central are used for viewing. Nothing is specific to one vendor's tooling.

6.1 Project, network and the bastion

  • One OpenStack project, with one private network. Every machine in the system is on that network.
  • One public entry point: the bastion holds the project's only floating IP. Every SSH connection, whether the operator's, the watcher's or the chat tunnel's, jumps through it.
  • The bastion is never joined to Kubernetes. If a cluster networking change ever broke things, the bastion stays clean and remains the way back in.
  • Workers have no public IP. They are reachable only from inside the private network.

6.2 Credentials

  • OpenStack credentials live in a single environment file on the operator workstation. It is loaded only by the commands that need it (the watcher, the feed and the reset tool) and is never printed or logged.
  • Credentials for the AI tooling are kept in a separate file, so the cloud credentials and the AI credentials never mix.
  • The k3s join token is never read by a person or a script. It is piped directly from the control node into a file on the joining machine that only its owner can read.
  • The database password stays in a Kubernetes secret. Connection strings are assembled on the cluster side and written straight into owner-only files.

6.3 Flavors: what we create and why

  • Standing machines use m7i.medium (2 vCPU, 4 GB RAM, 100 GB local NVMe).
  • Workers use m7i.2xlarge (8 vCPU, 32 GB RAM, 500 GB local NVMe), the smallest flavor with 8 cores.
  • OCR needs cores, not memory: a busy worker uses all 8 cores but only about 8–10 GB of RAM. The catalogue sells 8 cores only as a bundle with 32 GB and 500 GB, and nothing sits between the 2-core and 8-core sizes.
  • So the usage value counts the whole bundle. About 20% of a worker's valuation is the CPU the work needs; the rest is memory and disk that come with the flavor. We state this openly so the figure is not misread as what the work itself requires.
  • All machines boot Ubuntu 24.04 LTS from the cloud's standard image.

6.4 Creating a worker VM (Nova)

When the watcher decides another worker is needed, it calls a provisioning step that adds exactly one VM. Four checks run in order before the cloud is touched:

  1. Hard caps. The new worker must keep the project within 9 workers, 14 instances, 84 vCPU, 312 GB RAM and 5,100 GB local disk, and below the $478.80 abort value. Counts cover the whole project, not just the current run.
  2. A typed confirmation (PROVISION <run-id>). The operator's start-up script supplies it automatically, and only for the approved run.
  3. Manifest validation. Only the approved image, flavor, network and key pair are accepted.
  4. A write-gated API client. It is reused from an earlier Fixed Cost project, and its allowlist permits exactly one write call: create server (POST /servers). It cannot delete, resize or reconfigure anything.

The create request names the image, the flavor, the private network and the SSH key pair. It also attaches metadata tags:

  • managed_by and role=surge-worker
  • surge_run (the run ID)
  • created_seq (creation order)

Creates are made one at a time, at least 15 seconds apart, so two requests can never race past the caps.

6.5 Turning a new VM into a working cluster node

Once Nova reports the VM as ACTIVE, the rest happens in the background, so the watcher can launch the next machine without waiting:

  1. Wait for SSH through the bastion. A slow handshake on a booting VM counts as "not up yet," not as a failure.
  2. Preload the worker image. There is no container registry: an unauthenticated internal registry was rejected because it would have weakened TLS. Instead, the ingest image is copied to the new node as a file (3–5 seconds) and placed where k3s imports it automatically (13–27 seconds).
  3. Join k3s. The join token is piped from the control node straight into an owner-only file on the new node. The node then joins as a k3s agent.
  4. Label and taint. The node is labelled as a worker and tainted so that only ingestion pods run on it.
  5. Check service reachability. Before the node counts as ready, it must reach the cluster's internal services (cluster DNS and the database). In one test run, a worker joined and looked healthy but could never reach the database; this check now catches that case and restarts the node's k3s agent once to fix it.
  6. Start reading. The ingestion DaemonSet places a pod on the node, and the pod starts claiming pages.

Typical timing from the decision to the first page read is about 88 seconds:

StepTime
Nova create9 s
SSH ready28 s
Image copy4 s
k3s join19 s
Image import17–23 s
Pod start4 s

6.6 Identity: IDs and metadata, not names

  • Machines are identified by their OpenStack server ID and metadata tags. The names shown in Horizon are display-only (we renamed them once mid-project for clarity, and nothing broke).
  • The only tool that can delete machines checks IDs and metadata. It refuses all five standing machines by ID, by old name, by new name and by their role=standing tag.

6.7 Reading the Fixed Cost usage value

  • A read-only feed on the workstation polls the OpenStack server list every few seconds. For each server it reads the flavor and prices the allocation (vCPU, RAM and the flavor's local NVMe) with a small pricing module that reproduces the platform's own Fixed Cost valuation. A checksum guard stops that module from being changed without anyone noticing, so every figure comes from the same verified rates.
  • The feed powers the project's Fixed Cost dashboard. Its figures were checked to the cent against the platform's own valuation.
  • InMotion Central's own Plan & Usage page is the platform's view of the same thing. At the peak of the reference run, Central showed $432.03 (about 8.3% over) and our feed showed $430.56 (7.91%). Both value the same allocation; we have not traced the source of the small gap.
  • Horizon lists the new instances as they are created.

6.8 Coordinating with the cloud team

Before creating the full fleet, we asked the infrastructure team to confirm iad4 had room for the peak (14 instances, 82 vCPU, 308 GB RAM, 5,000 GB local disk). Two earlier test VMs had been scheduled onto one of the cloud's own control-plane hypervisors, which suggested tight headroom, and we did not want our burst to cause an outage for other tenants. The ramp was designed to stop on the first error or "no valid host" response.


7. The scaling watcher

The watcher runs on the operator's workstation and decides when to add workers.

Anticipatory scaling watcher decision loop polling backlog and drain rate, checking if projected finish exceeds the 20-minute deadline, verifying hard project caps, and either refusing at cap or launching one worker VM via Nova.
  • Deadline: fixed when the surge begins. It is the enqueue time of the oldest page still waiting, plus 20 minutes, and it does not move as new pages arrive.
  • Anticipatory, not reactive: the projection counts machines that are already booting and pages still arriving. The watcher doesn't wait for the backlog to grow before acting. An earlier version that joined workers one after another, waiting for each to finish, missed the deadline by 70 seconds.
  • Spacing: at least 15 seconds between creates.
  • Caps are code, not configuration. In every full test run, the watcher asked for a 10th worker and was refused, hundreds of times.
  • No scale-down. Keeping the added machines is the point of the design. The only path that deletes VMs is a separate, operator-run reset tool, used only between test runs. It requires a typed RESET <run-id> and is used only with the project owner's approval.

8. Fixed Cost mechanics

  • Plan: a flat $399 per month, with allowances of 24 vCPU, 82 GB RAM and 2,000 GB local NVMe.
  • The usage value is a valuation of what is allocated right now: vCPU, RAM and the flavor's local NVMe. It has no utilisation term and no duration term, so an idle VM counts the same as a busy one. It is not a bill.
  • Banner states:
Usage valueBanner
Up to $399Good
Above $399, up to $478.80 (20% over)Bursting
Above $478.80Plan check-in (our caps stop well short of it)
  • Nothing charges, caps or throttles at any level. If usage stays above plan for a week or two, someone from InMotion Cloud gets in touch to discuss a bigger plan. A day or two is fine.
  • The ladder (5 standing machines plus N workers), checked live to the cent:
WorkersUsage valueBanner
3 (start)$169.53Good
4$213.04Good
5$256.54Good
6$300.05Good
7$343.55Good
8$387.06Good
9 (peak)$430.56Bursting (+7.91%)
  • With three workers, before the surge, the vCPU ring already reads about 141% of its allowance (34 of 24 vCPU), while the overall usage value is still well under plan. Fixed Cost looks at the total value, not at each resource separately.

9. Keeping data local: the main design choice

The most important architectural decision was to keep the customer's documents inside their own cloud project. That choice drives where the processing power goes, and it is why the system needs more machines at all.

9.1 What stays inside the project, and what leaves

DataWhere it livesLeaves the project?
Original files and page imagesPostgres on surge-vector-dbNo
OCR textWorker memory, then database chunksNo
Embedding vectorspgvectorNo
Embedding and reranking modelsBaked into images on our VMsNo (no calls out)
A user's question, plus the few passages retrieved for itSent to the hosted LLM to write the answerYes: only this

The hosted model never sees the archive, the page images or the vectors. For each question it sees only the chat (question and earlier turns), the assistant's instructions and the handful of reranked passages relevant to that question.

9.2 Why not a cloud OCR service?

A managed OCR API would have removed most of the processing load. The workers would have had little to do, and three machines might have been enough.

We chose not to use one because:

  • Data locality. Every page of the customer's archive, including scans of contracts, records and reports, would be sent to a third party. For many customers that alone rules it out.
  • OCR is where the work is. It is the bottleneck by a wide margin, so outsourcing it means outsourcing most of the pipeline and most of the data.
  • Cost model. Cloud OCR is usually charged per page, so cost grows with every page in every archive. On a flat-fee plan, extra local capacity during a surge shows up as Bursting instead of a larger bill.
  • Control. Local OCR lets us pin versions, tune processes per core and keep throughput predictable, and it isn't subject to someone else's rate limits.

9.3 Why not a cloud embedding API?

This is the weaker half of the argument, and we concede it openly: embedding this archive through an API would cost only a dollar or two. But:

  • The text would still leave the project. To embed a passage, the API has to receive it, so the full text of every page goes out.
  • It would barely reduce the machines needed. Embedding is not the bottleneck: a worker needs only about 7 of its 21 chunks per second. Removing it would not meaningfully change the worker count.
  • Questions must use the same model. Every question is embedded with the same model as the documents. A cloud embedding API would therefore put a network call on every question as well.
  • The local model is small and fast. bge-small-en-v1.5 runs well on CPU and is baked into the image, so it adds no download at startup and no outside dependency.

9.4 The hosted LLM, and replacing it with a local model

We use a hosted model (claude-haiku-4-5) only to write answers, and it receives only the question and the relevant passages (§9.1). That keeps exposure small: a few paragraphs per question, never the archive.

It can also be removed entirely. The model adapter already has a generic driver for any OpenAI-compatible endpoint, which is what llama.cpp, vLLM and Ollama all expose. Switching to a model running inside the project is a configuration change; Hermes, the retrieval tool and the guardrails stay the same.

Why we didn't do that in this build is about speed, not capability:

Local model on one m7i.2xlarge (CPU only, 4-bit)GenerationTime to read a 2,000-token RAG prompt
Gemma 3 4B15.2 tokens/sabout 23 s
Gemma 3 12B5.8 tokens/sabout 70 s
  • On CPU, the slow part of a RAG answer is reading the prompt (the retrieved passages), not writing the answer. A 12B model would wait about 70 seconds before writing its first word.
  • A small model is quick enough but less reliable at tool calls and at following the citation and refusal rules.
  • Hosted, a single-fact answer took about 10 seconds and a two-paper answer about 15.

When a fully local model makes sense:

  • The data must not leave the project at all.
  • A GPU-backed machine is available. We didn't measure one, but reading the prompt is exactly what GPUs speed up.
  • Slower answers are acceptable, for example in batch or overnight use.

The groundedness guard and the refusal rule still apply in a local setup. If anything they matter more, because smaller models invent numbers more often.

9.5 Choosing the workload and the test data

The surge had to come from work that genuinely needs more machines. We measured the candidates before choosing:

CandidateFinding
Chat trafficA chat request needs about 56 ms of local work and then waits on the hosted model. Even heavy chat traffic never needs more VMs, so scaling for it would only add idle machines.
Reranking loadWith the fast reranker, nine workers could serve thousands of concurrent users. Only a deliberately slow model would make reranking the bottleneck, and that would be artificial load.
Document ingestion (chosen)OCR is CPU-bound, adding threads doesn't help, and only more machines raise throughput.
Test-data optionWhy we rejected it
A generated synthetic corpusAbout 20 million tokens of generation, and the answers couldn't be checked against a real source.
RVL-CDIP scanned-document datasetNo clear licence of its own, and it contains tobacco-litigation documents with real signatures.

9.6 Other design alternatives we rejected

AlternativeWhy we rejected it
Graph-style indexing (LightRAG, GraphRAG)It needs about 333,000 language-model calls per archive, which is expensive and runs against the point of a flat-fee plan.
A per-node queue (SQLite on each node)New machines joined with nothing to do.
"Pause pod" headroom triggerIt could never fire, because the ingestion pod takes the whole node.
An internal container registryAn unauthenticated registry would have weakened TLS. Images are preloaded per node instead.
Event-driven pod autoscaling (KEDA)Superseded. Pods were never the limit; machines were.
Automatic scale-downAgainst the point of the project. Machines stay; deletes are manual, gated and between test runs only.

10. Results

10.1 Component measurements

MeasurementResult
OCR accuracy on rendered scans97.7% characters, 95.7% words
Ingestion per worker, in clusterabout 2.0 pages/s, 85–90% CPU
Three workers on one shared backlog6.01 pages/s, even split, zero failures
Upload into the queue35.8 MB/s, 41 pages/s
Decision to worker readingabout 88 s
Peak drain at nine workersabout 19 pages/s
Answer time (hosted model)single fact about 10 s; two papers about 15 s
Tool-call reliability (claude-haiku-4-5)10 of 10

10.2 Answer quality

We graded a set of 19 questions against the corpus evidence, three runs each: single facts, two- and three-paper questions, comparisons and trap questions whose answers are not in the archive.

  • 26 of 26 answerable parts were correct, each with a real citation.
  • 12 of 12 trap questions were correctly refused.
  • 0 confident wrong answers once the groundedness guard was in place.

10.3 Earlier test runs: how we got to "on time"

RunChange being testedResult against the 20-minute deadline
drkj (30 Sep)Workers joined one after anotherMissed by about 1 min 10 s
ahdn (1 Oct)Anticipatory watcher, joins in backgroundMade it by about 20 s, with one worker fixed by hand
zryo (1 Oct)Image preloaded into k3s, fully unattendedMissed by 40 s: one worker could not reach the database
yote (1 Oct)Plus the reachability checkMade it by 86 s, no manual step

10.4 The reference run

Times are measured from the moment the surge began (the first page waiting), using the watcher and feed logs.

TimeEvent
+0:05Watcher projects a missed deadline; it decides to add worker 4
+0:18Worker 4 is ACTIVE in OpenStack
+1:42Usage value reaches $430.56 — Bursting (14 instances, 82 vCPU)
+1:47Worker 9 is ACTIVE
+1:57The watcher asks for a 10th worker; the caps refuse it (and keep refusing)
+3:12All nine workers are reading pages
+3:22Backlog peaks at 11,706 pages
+9:28The projected finish is back inside the deadline: done in 9 min 2 s, with 10 min 31 s left
+18:53Last page read, about 67 seconds before the deadline

After ingestion finished:

  • The control question (§2), refused before the upload, was answered correctly: serum asprosin was high in 96% of patients on the first day [PMC10000515]. We checked the figure against the paper on PMC.
  • "What proportion of the healthcare workers had sleep problems?" was answered with 59.6% and a citation, and checked the same way.
  • A two-paper question was answered from two different papers, each cited: "What percentage of pregnant women in Bangladesh are anemic, and what proportion of women in Japan had a partner-accompanied birth during the pandemic?"
  • Fee: $399. Banner: Bursting. All nine workers were left running.

11. Lessons learned

Almost every serious bug passed the offline test suite and showed up only on real machines.

  1. Thread limits matter enormously. Eight OCR processes each spreading threads across all eight cores ran 148 times slower than eight single-threaded ones.
  2. Silent success is the worst failure. At one point every chunk failed to save while the run reported success. Failures are now loud and stop the run.
  3. Citations need per-document identity. At one point all chunks shared one document ID, which made every citation meaningless.
  4. Queues belong where new machines can see them. A queue on each node meant new nodes had nothing to do.
  5. Connection handling matters. Eight threads sharing one database connection read one page at a time. One connection per thread then leaked until the database refused new clients.
  6. Batch by bytes, not by count. Fifty TIFF pages is about 55 MB, and one such batch crashed a database process. Batches now also flush at 8 MB.
  7. Truncated context makes models invent things. With 400-character snippets, the model produced a plausible but wrong figure beside a correct citation. The fix was full passages plus the groundedness guard.
  8. Sessions leak across tests. A cached agent session "remembered" the control answer from an earlier run and answered before the archive was indexed. Each run now starts with cleared sessions and unique session IDs.
  9. Being on time isn't enough if each step blocks. A trigger that fired at the right moment still missed the deadline because each machine's join blocked the next. Joins now run in the background.
  10. "Ready" is not the same as "working." A node can be Ready in Kubernetes and still unable to reach the database. The join now checks that services are reachable.
  11. Secrets belong in pipes, not on command lines. An early version put the join token in a command argument and in an evidence log. Tokens now go node to node through pipes.

12. Security posture

  • One way in: a single bastion with the only public IP. Workers, the database and the assistant have no public addresses.
  • Least-privilege cloud writes: the provisioning client can only create servers, within hard caps and with a typed confirmation. The read-only feed can only list servers.
  • Deletes are separate and gated: one operator tool, a typed confirmation, workers only, refusing standing machines by ID and tag, and only with the owner's approval.
  • Secrets never pass through a terminal: cloud credentials are loaded only where needed, join tokens and database credentials move by pipe into owner-only files, and nothing is echoed or logged.
  • Restricted tunnel for chat: forwarding-only keys locked to one destination each, pinned host keys and an API key on top.
  • A minimal agent: every built-in Hermes tool (web, browser, terminal, files, code, memory) is disabled. The only thing it can do is search the archive.
  • Workload isolation: OCR nodes are tainted, the database has guaranteed resources, and the assistant can't be scheduled onto a worker.

13. Limitations and future work

  • Thin margin. With all nine workers reading, there are about 60–90 seconds to spare. A worker that joins but can't read costs about 2 minutes. A production promise would add headroom or say "about twenty minutes."
  • Bundled flavors. Workers need cores, not memory. A CPU-heavy flavor (8 vCPU with less RAM) would bring the usage value closer to what the work actually uses.
  • Local model option. Benchmark a GPU-backed flavor, if one becomes available, to make a fully in-project answer step practical.
  • Housekeeping: remove the unused registry node, skip duplicate chunks (same paper, same text) at write time and add per-host capacity read access for pre-flight checks.

14. Conclusion

We set out to show something that is easy to claim and hard to prove: that an AI system on InMotion Cloud can handle a real, CPU-heavy surge by adding machines on its own, and that a flat-fee plan absorbs it.

  • The workload was real. About 17,000 licensed, OCR-heavy pages, every one read, chunked, embedded and indexed on our own VMs.
  • The scaling was real. A watcher projected the miss within seconds, created six VMs through the OpenStack API within two minutes and had all nine workers reading just over three minutes into the surge. It met the 20-minute promise with about a minute to spare. Hard caps refused every request past nine workers.
  • The answers were real. They are cited to PMC papers anyone can check, and the system says "I don't have anything on that" when the archive has nothing.
  • The data stayed local. OCR, embedding, search and reranking all ran inside the customer's project. Only a question and its few relevant passages went to a hosted model, and that step can also run in-project.
  • The plan behaved as promised. The usage value went from $169.53 to $430.56 and the banner showed Bursting. The fee stayed $399, nothing was charged, and the extra machines were left running.

The short spike is not the benefit; any cloud handles a 20-minute spike cheaply. The benefit is that you can keep the capacity while you find out whether the surge was a one-off or the new normal, and that time costs nothing extra.

Looking for a concise summary to share with your team? Read the 7-minute overview of Surging RAG.


Appendix A — Glossary

TermMeaning
RAGRetrieval-augmented generation: find relevant passages first, then have a language model answer from them.
OCROptical character recognition: turning images of text into text.
EmbeddingTurning text into a list of numbers (a vector) so similar meanings sit close together.
pgvectorA Postgres extension for storing vectors and searching them by similarity.
RerankerA model that rescores search results to put the most relevant first.
k3sA lightweight Kubernetes distribution.
DaemonSetA Kubernetes rule that runs one copy of a pod on every matching node.
TaintA Kubernetes marker that keeps unrelated pods off a node.
BastionA single hardened machine that is the only way in from outside.
NovaOpenStack's compute service, which creates and manages VMs.
HorizonOpenStack's web dashboard.
MCPModel Context Protocol, a standard way to give an AI agent tools.
Fixed Cost AdvantageInMotion Cloud's flat-fee plan, which allows usage above the plan allowance without extra charges.
AI Engineer

I love to tinker with cool stuff.

Share this Article
RELATED RESOURCES

Let’s map your infrastructure to the right plan

A free 30-minute call with our cloud team. We review your current environment and show you exactly what a move to InMotion Cloud would look like: timeline, architecture, and cost.

Compare plans