{"slug": "stress-testing-repo-rag-against-real-github-repositories-from-99-files-to-a-12k", "title": "🧪 Stress-Testing repo-rag Against Real GitHub Repositories — From 99 Files to a 12k-File Monster", "summary": "A stress test of the repo-rag MCP server against four real GitHub repositories — gin, fiber, beego, and microsoft/vscode — found one bug, three architectural limits, and a keyless AI mode. The test measured cold start times ranging from 37.6 seconds for gin (99 files) to over 85 minutes for vscode (12,614 files, aborted), with peak RSS exceeding 1 GB for vscode. The bug, a nondeterministic argument binding order caused by Go map iteration, was fixed by making the schema's required list use []interface{}.", "body_md": "[← All posts← Alle Beiträge](../blog.html)\n\n# 🧪 Stress-Testing repo-rag Against Real GitHub Repositories — From 99 Files to a 12k-File Monster\n\n**We pointed **\n\n`repo_rag_server`\n\nat four real repositories — gin, fiber, beego and microsoft/vscode — drove it with raw JSON-RPC over stdio, measured every phase with byte-level I/O accounting, and found one real bug, three architectural limits and a surprisingly capable keyless AI mode. This is the full lab report.> **Related reading:** [repo-rag MCP](repo-rag-mcp.html) — the server under test · [pipe-docs MCP](pipe-docs-mcp.html) — the architecture it generalizes\n\nEverything you read below was measured against the actual MCP server process — not unit fixtures, not mocks. The question was simple: **at what repository size does this design stop working, and why?**\n\n## 🔬 The setup: a measuring harness, not a vibe check\n\nWe wrote a small Python harness (`harness.py`\n\n) that:\n\n- spawns the real\n`pipe examples/repo_rag_server.pipe`\n\nas a subprocess, - speaks newline-delimited JSON-RPC over stdio exactly like an MCP host,\n- polls\n`/proc/<pid>/status`\n\n(peak RSS) and`/proc/<pid>/io`\n\n(bytes read/written) while the server works, - timestamps everything: spawn →\n`initialize`\n\nresponse = full clone+prune+index build time.\n\nNo API key was configured for most runs, so the server operated in its **keyword-only mode** — the baseline every user gets out of the box. Repos lived in isolated cache directories so we could measure four distinct phases independently:\n\n| Phase | How |\n|---|---|\n| Cold start | fresh cache → clone + junk-prune + both index builds |\n| Warm start | untouched cache → SHA-256 hash checks only |\n| Reindex-only | `.db` files deleted, checkout kept |\n| Incremental | DBs restored, a handful of files touched |\n\n## 📊 The ladder: gin → fiber → beego → vscode\n\n| gin | fiber | beego | vscode (aborted) | |\n|---|---|---|---|---|\n| Source files | 99 | 303 | 365 | 12,614 |\n| Code symbols | 1,617 | 6,017 | 4,528 | ~39,000+ (est.) |\n| Markdown chunks | 124 | 776 | 35 | – |\nCold start | 37.6 s | 245.8 s | 90.5 s | >85 min, unfinished |\n| Warm start | 7.3 s | 36.2 s | 38.5 s | – |\n| Reindex-only | – | 159.9 s | 93.1 s | – |\n| Incremental (+5 files) | – | 51.6 s | 35.8 s | – |\n`refresh_index` (warm) | 2.2 s | 22.0 s | 18.0 s | – |\n| Peak RSS | 33 MB | 104 MB | 60 MB | >1 GB |\n`search_code` latency | 30–60 ms | 90–140 ms | 70–95 ms | – |\n\nCorrectness held up everywhere: symbol lookups verified against `grep`\n\nhit the exact file and line (`newViewsLockStore`\n\n→ `app.go:148`\n\n, `RunWithMiddleWares`\n\n→ `server/web/beego.go:59`\n\n), path traversal (`../../etc/passwd`\n\n) was cleanly rejected, unknown tools returned proper JSON-RPC errors.\n\n## 🐛 Finding 1: The bug that appeared every other run\n\nThe first smoke test produced something stranger than slowness: `read_source`\n\ncrashed with `replace_all: first argument must be a string`\n\n— but only sometimes. Four identical restarts: OK, crash, OK, crash.\n\nRoot cause: when registering tools via `ai_tool`\n\n, the parameter order used for **positional argument binding** was built by iterating a Go map — whose iteration order is deliberately randomized. With two parameters (`offset`\n\n, `path`\n\n), roughly half of all processes bound the arguments swapped, feeding the number `1`\n\ninto a string function.\n\nThe fix made the schema's `required`\n\nlist use the same element type JSON unmarshalling produces (`[]interface{}`\n\ninstead of `[]string`\n\n), so every consumer can type-assert uniformly — binding order is now deterministically alphabetical, guarded by a regression test that registers six deliberately unsorted parameters and asserts their arrival order.\n\n## 🐌 Finding 2: O(n²) hash checks — where cold starts go to die\n\nPer file, the sync does `SELECT hash FROM files WHERE path = ?`\n\n. Sounds innocent — except the pure-Pipe SQLite engine answers queries with a **full table scan**, and the `files`\n\ntable grows with every indexed file. File #12,000 scans up to 12,000 interpreted rows before its own insert.\n\nThe signature of this is unmistakable in the process telemetry: long phases of hot CPU with near-zero read I/O and flat memory. At fiber's scale (303 files ≈ ~46k row comparisons) it costs seconds; extrapolating to vscode's 12.6k files (~80M comparisons) it dominates everything. The irony: reading and parsing all of gin took ~25 s — the *checking whether we need to* part scales worse than the work itself. A plain in-memory map lookup would turn this O(n²) into O(n).\n\n## 💾 Finding 3: Memory grows with the whole repo — and crashes lose everything\n\nPeak RSS scaled at roughly **15–18 KB per symbol** across all repos: 33 MB (gin, 1.6k symbols) → 104 MB (fiber, 6k) → 60 MB (beego, 4.5k). That's linear and predictable — until you multiply it by a monorepo.\n\nvscode: after ~85 minutes the build was still running, memory had blown past **1 GB** and was climbing at 200 MB per 30 seconds. Then the test machine (a phone-class ARM device) collapsed. And here is the harsh part: **everything was lost**. The pure-Pipe SQLite module persists only on `db_close`\n\n, which happens once, after the entire index is built. No checkpoint, no journal, no partial state — 85 minutes of scanning evaporated with the process.\n\n## ⏱️ Finding 4: Warm starts are racing your MCP host's timeout\n\nMCP hosts typically give a stdio server 30–60 s to answer `initialize`\n\n. repo-rag builds **all indexes before serving** — by design, so it never serves stale data. The consequence shows in the ladder: already at ~400 files, a warm start takes 36–39 s. Not because anything is slow per se, but because every file gets hashed and every symbol loaded before the first response. Combined with Finding 2, cold starts at vscode scale are simply beyond what any host will wait for.\n\n## 🤖 Keyless AI: surprisingly capable (coming in the next release)\n\n> **Note:** The **opencode provider described here is brand new and will ship with the next Pipe release** — it is merged on `master`\n\nbut not part of v1.1.1.\n\nPipe gained a fourth AI provider: **OpenCode Zen** (`opencode.ai/zen`\n\n). Its party trick: a free public tier that works **without any API key** — requests authenticate via CLI-mimicking headers, and free models (`big-pickle`\n\n, `-free`\n\nsuffixes) cost exactly $0.00 in the sandbox budget accounting.\n\nWe wired it into repo-rag's provider chain (opt-in via `REPO_RAG_AI=opencode`\n\n, model override via `REPO_RAG_MODEL`\n\n) and re-ran gin **with AI enabled**:\n\n```\n# keyless public tier, free model\nexport REPO_RAG_URL=\"https://github.com/gin-gonic/gin\"\nexport REPO_RAG_AI=\"opencode\"\nexport REPO_RAG_MODEL=\"mimo-v2.5-free\"   # big-pickle was 503 during our window\npipe examples/repo_rag_server.pipe\n```\n\n- Embeddings run locally for this provider, so building the semantic index added\n**nothing measurable** to startup (7.41 s, same as keyword-only). - Semantic\n`search_docs`\n\ngot**faster** than keyword mode (10–39 ms vs 65 ms) because embedding lookup avoids the chunk-table scan entirely. `ask_docs`\n\nanswered five real questions about gin in 9–20 s each:\n\n| Question | Verdict |\n|---|---|\n| Create a router with default middleware | ✅ correct, complete, cited |\n| Recovery middleware internals | ⚠️ honest refusal — \"not in the provided context\" |\n| JSON binding + validation errors | ✅ correct method, tags, error handling |\n| Route groups with group-scoped middleware | ✅ correct pattern |\n| JSON libraries via build tags | ✅ exactly the documented trio (jsoniter/go_json/sonic) |\n\nZero hallucinations. The one miss is telling: gin's docs *do* contain a `CustomRecovery`\n\nexample — the retrieval layer just didn't surface it (local embeddings + only six candidates). The model correctly refused instead of inventing. Better recall (more candidates, heading boosts) would have turned that refusal into a hit.\n\nOne operational note: the free tier rotates models — `big-pickle`\n\nanswered with a 503 upstream error during our window, while `mimo-v2.5-free`\n\nworked flawlessly. Set `REPO_RAG_MODEL=mimo-v2.5-free`\n\nand you're productive again.\n\n## 🔧 What we'd fix next\n\n**Replace the per-file SQL hash check with an in-memory map**— turns the dominant O(n²) cost into O(n) and makes vscode-class repos feasible.** Persist incrementally**(checkpoint every N files) — converts a crash from total loss into a resume.** Support optional tool arguments properly**(schema`required`\n\nlists per tool) so the argument-shift normalizations become reachable.**Raise** to close retrieval gaps like the`ask_docs`\n\ncandidate count or add heading boosts`CustomRecovery`\n\ncase.\n\n## ✅ Verdict\n\nFor its sweet spot — repositories up to a few hundred source files — repo-rag delivers exactly what it promises: correct symbol search, clean path safety, graceful degradation without keys, and (with the upcoming Zen provider) cited AI answers for free. Past ~10k files, the current architecture hits a wall that is architectural, not incidental — and now there are numbers that show precisely where and why.\n\nThat is what stress tests are for.\n\n# 🧪 repo-rag unter Dauerbeschuss mit echten GitHub-Repos — von 99 Files bis zum 12k-Files-Monster\n\n**Wir haben **\n\n`repo_rag_server`\n\nauf vier echte Repositorys losgelassen — gin, fiber, beego und microsoft/vscode —, ihn mit rohem JSON-RPC über stdio getrieben, jede Phase mit Byte-Level-I/O-Accounting gemessen und dabei einen echten Bug, drei architektonische Grenzen und einen überraschend fähigen schlüssellosen KI-Modus gefunden. Das ist der komplette Laborbericht.> **Verwandte Lektüre:** [repo-rag MCP](repo-rag-mcp.html) — der getestete Server · [pipe-docs MCP](pipe-docs-mcp.html) — die Architektur, die er verallgemeinert\n\nAlles unten wurde gegen den echten MCP-Server-Prozess gemessen — keine Unit-Fixtures, keine Mocks. Die Frage war simpel: **Bei welcher Repository-Größe hört dieses Design auf zu funktionieren, und warum?**\n\n## 🔬 Das Setup: Mess-Harness statt Bauchgefühl\n\nEin kleiner Python-Harness:\n\n- startet den echten\n`pipe examples/repo_rag_server.pipe`\n\nals Subprozess, - spricht newline-delimited JSON-RPC über stdio wie ein echter MCP-Host,\n- pollt\n`/proc/<pid>/status`\n\n(Peak-RSS) und`/proc/<pid>/io`\n\n(gelesene/geschriebene Bytes), - stempelt alles: Spawn →\n`initialize`\n\n-Antwort = komplette Clone+Prune+Index-Build-Zeit.\n\nKein API-Key war konfiguriert, der Server lief also im **Keyword-only-Modus** — die Basis, die jeder Nutzer out-of-the-box bekommt. Isolierte Cache-Verzeichnisse machten vier Phasen unabhängig messbar: Kaltstart, Warmstart, Reindex-only (`.db`\n\ns gelöscht) und inkrementell (wenige Files angefasst).\n\n## 📊 Die Treppe: gin → fiber → beego → vscode\n\n| gin | fiber | beego | vscode (abgebrochen) | |\n|---|---|---|---|---|\n| Source-Files | 99 | 303 | 365 | 12.614 |\n| Code-Symbole | 1.617 | 6.017 | 4.528 | ~39.000+ (geschätzt) |\n| Markdown-Chunks | 124 | 776 | 35 | – |\nKaltstart | 37,6 s | 245,8 s | 90,5 s | >85 min, nicht fertig |\n| Warmstart | 7,3 s | 36,2 s | 38,5 s | – |\n| Reindex-only | – | 159,9 s | 93,1 s | – |\n| Inkrementell (+5 Files) | – | 51,6 s | 35,8 s | – |\n| Peak-RSS | 33 MB | 104 MB | 60 MB | >1 GB |\n\nDie Korrektheit hielt überall stand: Symbol-Lookups gegen `grep`\n\nverifiziert trafen exakt Datei und Zeile, Path-Traversal wurde sauber abgewiesen, unbekannte Tools lieferten korrekte JSON-RPC-Fehler.\n\n## 🐛 Fund 1: Der Bug, der nur bei jedem zweiten Start erschien\n\nDer erste Smoke-Test produzierte etwas Seltsameres als Langsamkeit: `read_source`\n\ncrashte mit einem Typfehler — aber nur manchmal. Vier identische Restarts: OK, Crash, OK, Crash.\n\nDie Ursache: Beim Tool-Registering über `ai_tool`\n\nwurde die Parameterreihenfolge für das **positionale Argument-Binding** aus einer Go-Map-Iteration gebaut — deren Reihenfolge ist absichtlich zufällig. Bei zwei Parametern (`offset`\n\n, `path`\n\n) vertauschte etwa die Hälfte aller Prozesse die Argumente und fütterte eine Zahl in eine Stringfunktion.\n\nDer Fix bringt das Schema-`required`\n\nauf denselben Elementtyp, den JSON-Unmarshalling erzeugt (`[]interface{}`\n\nstatt `[]string`\n\n), sodass alle Konsumenten einheitlich type-asserten können — die Binding-Order ist jetzt deterministisch alphabetisch, abgesichert durch einen Regressionstest mit sechs bewusst unsortierten Parametern.\n\n## 🐌 Fund 2: O(n²)-Hash-Checks — wo Kaltstarts sterben\n\nPro File macht der Sync `SELECT hash FROM files WHERE path = ?`\n\n. Klingt harmlos — nur antwortet die pure-Pipe-SQLite-Engine auf Queries mit einem **Full-Table-Scan**, und die `files`\n\n-Tabelle wächst mit jedem indizierten File. File #12.000 scannt bis zu 12.000 interpretierte Rows vor seinem eigenen Insert.\n\nDas Muster ist in der Telemetrie unverkennbar: lange Phasen heißer CPU bei nahezu null Lese-I/O und flachem RAM. Bei fibers Größe (303 Files ≈ ~46k Row-Vergleiche) kostet das Sekunden; hochgerechnet auf vsodes 12.6k Files (~80 Mio. Vergleiche) dominiert es alles. Die Ironie: Alle gin-Files zu lesen und parsen dauerte ~25 s — das *Prüfen, ob wir müssen*, skaliert schlechter als die Arbeit selbst. Ein simpler In-Memory-Map-Lookup würde aus O(n²) ein O(n) machen.\n\n## 💾 Fund 3: RAM wächst mit dem ganzen Repo — und Crashes verlieren alles\n\nDer Peak-RSS skalierte mit grob **15–18 KB pro Symbol**: 33 MB (gin) → 104 MB (fiber) → 60 MB (beego). Linear und berechenbar — bis man ihn mit einem Monorepo multipliziert.\n\nvscode: Nach ~85 Minuten lief der Build noch, der Speicher war über **1 GB** und stieg um 200 MB pro 30 Sekunden. Dann kollabierte die Testmaschine (ein Handy-Klasse-ARM-Gerät). Und das Harte daran: **Alles war verloren.** Das pure-Pipe-SQLite persistiert ausschließlich bei `db_close`\n\n— einmal, nach dem kompletten Index-Build. Kein Checkpoint, kein Journal, kein Teilststand: 85 Minuten Scannen verdampften mit dem Prozess.\n\n## ⏱️ Fund 4: Warmstarts rasen gegen den Startup-Timeout deines MCP-Hosts\n\nMCP-Hosts geben einem Stdio-Server typischerweise 30–60 s Zeit für die `initialize`\n\n-Antwort. repo-rag baut **alle Indizes vor dem Serven** — absichtlich, um nie veraltete Daten zu liefern. Die Folge zeigt die Treppe: Schon bei ~400 Files dauert ein Warmstart 36–39 s. Kombiniert mit Fund 2 sind Kaltstarts in vscode-Größe schlicht jenseits dessen, worauf ein Host wartet.\n\n## 🤖 Schlüssellose KI: überraschend fähig (kommt mit dem nächsten Release)\n\n> **Hinweis:** Der hier beschriebene **opencode-Provider ist brandneu und erscheint erst mit dem nächsten Pipe-Release** — er ist auf `master`\n\ngemerged, aber nicht Teil von v1.1.1.\n\nPipe bekommt einen vierten AI-Provider: **OpenCode Zen** (`opencode.ai/zen`\n\n). Sein Party-Trick: ein kostenloser Public-Tier, der **ohne jeden API-Key** funktioniert — Requests authentifizieren sich über CLI-nachempfundene Header, und Free-Modelle (`big-pickle`\n\n, `-free`\n\n-Suffixe) kosten exakt $0,00 im Sandbox-Budget.\n\nIn repo-rags Provider-Chain eingebunden (Opt-in über `REPO_RAG_AI=opencode`\n\n, Modell-Override über `REPO_RAG_MODEL`\n\n) haben wir gin **mit KI** neu gefahren:\n\n```\n# schlüsselloser Public-Tier, freies Modell\nexport REPO_RAG_URL=\"https://github.com/gin-gonic/gin\"\nexport REPO_RAG_AI=\"opencode\"\nexport REPO_RAG_MODEL=\"mimo-v2.5-free\"   # big-pickle lief in unserem Fenster auf 503\npipe examples/repo_rag_server.pipe\n```\n\n- Embeddings laufen bei diesem Provider lokal — der semantische Index kostete\n**nichts Messbares** an Startzeit (7,41 s, gleichauf mit Keyword-only). - Semantische\n`search_docs`\n\nwurden**schneller** als der Keyword-Modus (10–39 ms vs 65 ms), weil Embedding-Lookup den Chunk-Table-Scan komplett umgeht. `ask_docs`\n\nbeantwortete fünf echte gin-Fragen in je 9–20 s:\n\n| Frage | Urteil |\n|---|---|\n| Router mit Default-Middleware erstellen | ✅ korrekt, vollständig, zitiert |\n| Recovery-Middleware intern | ⚠️ ehrliche Verweigerung — „nicht im Kontext enthalten\" |\n| JSON-Binding + Validierungsfehler | ✅ korrekte Methode, Tags, Fehlerbehandlung |\n| Route-Groups mit gruppenweisem Middleware-Scope | ✅ korrektes Muster |\n| JSON-Bibliotheken via Build-Tags | ✅ exakt das dokumentierte Trio (jsoniter/go_json/sonic) |\n\nNull Halluzinationen. Der eine Fehltreffer ist aufschlussreich: gins Docs *enthalten* ein `CustomRecovery`\n\n-Beispiel — nur die Retrieval-Schicht hob es nicht hervor (lokale Embeddings + nur sechs Kandidaten). Das Modell hat korrekt verweigert statt zu erfinden. Besseres Recall (mehr Kandidaten, Heading-Boosts) hätte aus der Verweigerung einen Treffer gemacht.\n\nEine operative Anmerkung: Der Free-Tier rotiert Modelle — `big-pickle`\n\nantwortete in unserem Zeitfenster mit einem 503-Upstream-Fehler, während `mimo-v2.5-free`\n\neinwandfrei lief. Ein `REPO_RAG_MODEL=mimo-v2.5-free`\n\nmacht dich wieder produktiv.\n\n## 🔧 Was wir als Nächstes fixen würden\n\n**Den per-File-SQL-Hash-Check durch eine In-Memory-Map ersetzen**— macht aus der dominierenden O(n²)-Kostenstelle ein O(n) und machen vscode-Klasse-mäßige Repos machbar.** Inkrementell persistieren**(Checkpoint alle N Files) — verwandelt einen Crash von Totalverlust in Wiederaufnahme.** Optionale Tool-Argumente sauber unterstützen**(pro-Tool-`required`\n\n-Listen), damit die Argument-Shift-Normalisierungen überhaupt erreichbar werden., um Retrieval-Lücken wie den`ask_docs`\n\n-Kandidatenzahl erhöhen oder Heading-Boosts ergänzen`CustomRecovery`\n\n-Fall zu schließen.\n\n## ✅ Fazit\n\nIn ihrer Sweet-Spot-Zone — Repositorys bis einige hundert Source-Files — liefert repo-rag genau, was sie verspricht: korrekte Symbolsuche, saubere Path-Safety, graceful Degradation ohne Keys und (mit dem kommenden Zen-Provider) zitierte KI-Antworten gratis. Jenseits von ~10k Files stößt die aktuelle Architektur an eine Wand, die architektonisch, nicht zufällig ist — und jetzt gibt es Zahlen, die exakt zeigen, wo und warum.\n\nDafür sind Stresstests da.", "url": "https://wpnews.pro/news/stress-testing-repo-rag-against-real-github-repositories-from-99-files-to-a-12k", "canonical_source": "https://pipe-lang.com/blog/repo-rag-stress.html", "published_at": "2026-08-23 00:00:00+00:00", "updated_at": "2026-08-24 00:13:31.603017+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools", "ai-infrastructure"], "entities": ["repo-rag", "gin", "fiber", "beego", "microsoft/vscode", "repo_rag_server", "pipe-docs"], "alternates": {"html": "https://wpnews.pro/news/stress-testing-repo-rag-against-real-github-repositories-from-99-files-to-a-12k", "markdown": "https://wpnews.pro/news/stress-testing-repo-rag-against-real-github-repositories-from-99-files-to-a-12k.md", "text": "https://wpnews.pro/news/stress-testing-repo-rag-against-real-github-repositories-from-99-files-to-a-12k.txt", "jsonld": "https://wpnews.pro/news/stress-testing-repo-rag-against-real-github-repositories-from-99-files-to-a-12k.jsonld"}}