Parsing is the easy part
How we handle ten million lines of code without melting your CPU or the cloud bill.
By git11 Engineering · Mon Feb 16 2026 · 7 min read
Most GitHub apps are slow because they treat code like text. They grep strings and hope for the best.
If you want to analyze a repository in three seconds, grepping is a death sentence. To actually understand what a codebase does—to know that User.find() in controllers/auth.go refers to the definition in models/user.go—you can’t just look at files. You have to build a graph.
At git11, we spent six months trying to make this work at scale. Here is how we stopped fighting the hardware and started using it.
Concrete over abstract
Everyone starts with Abstract Syntax Trees (ASTs). They’re the standard. They’re also a massive memory hog if you’re not careful.
If you load the AST for a 5,000-line React component into a standard Node.js or Python heap, you’ve already lost. The overhead of object allocation will kill your latency before the first analysis rule even runs.
We don't use high-level object trees. We use Tree-sitter. It’s written in C, it has bindings for everything, and it’s incrementally stable.
Tree-sitter doesn't just parse; it manages byte-offsets in a way that allows us to re-parse only the changed sections of a file when a dev pushes a new commit. If you change one line in a 10k LOC file, we shouldn't have to re-evaluate the other 9,999.
The symbol table is a database
Parsing a file into an AST is the easy part. The hard part is name resolution.
When you see api.FetchData(), you need to know if api is a local variable, an imported module, or a global singleton. Doing this locally within a single file is trivial. Doing it across 500 microservices is where most tools break.
We shifted from keeping symbols in memory to using a specialized version of a KV store (we use RocksDB) indexed by content hashes.
Instead of "This file contains this class," we store "This hash of code represents this interface." This allows us to cache analysis results globally. If two different users have the same version of lodash in their repo, we analyze it exactly once for the entire platform.
Why Rust actually matters
We didn't choose Rust because it's trendy. We chose it because we need to control the memory layout of our nodes.
In Go or TypeScript, you have very little control over how your data structures sit in L1/L2 cache. When you're traversing a graph with millions of nodes, cache misses are the primary bottleneck.
By using SoA (Structure of Arrays) instead of AoS (Array of Structures), we keep the hot data—like node types and byte ranges—packed tightly in memory. This lets the CPU pre-fetch data effectively.
If your analysis engine is spending 60% of its time waiting for a pointer to resolve from main memory, you're doing it wrong.
Moving logic to the edge of the disk
We see people trying to build these tools using a "Read -> Parse -> Analyze -> Write" pipeline. This is too slow.
We use a streaming architecture. As blocks of bytes come off the disk (or the GitHub API stream), they are piped directly into the parser. Analysis rules run as soon as a node is closed.
We don't wait for the file to finish. If we find a security vulnerability at line 10, we want to know it before line 1000 is even out of the buffer.
Language Server Protocol is a lie
LSP is great for IDEs. It’s terrible for bulk analysis.
LSP assumes a stateful connection between a user and a single workspace. It wasn't built for high-throughput headless scanning.
If you try to spin up 500 instances of pyright or tsserver to analyze a massive repo, your orchestrator will catch fire. These servers are heavy. They have long startup times.
We ended up writing our own light-weight implementations for core language features. We don't need to support every LSP feature like "rename symbol." We just need fast, parallelizable fact extraction.
Parallelism is hard to do right
Giving a task 32 cores doesn't make it 32x faster. Usually, it just creates 31 cores' worth of lock contention.
We use a work-stealing scheduler. Every worker thread has its own local queue of files to parse. When one thread finishes, it steals work from the others.
Crucially, we shard the symbol table. Workers don't fight over a single global lock to register a new class definition. They write to thread-local buffers that get merged in a single, final pass.
Stop over-thinking the 'AI' part
Everyone wants to throw the whole codebase into a Large Language Model. Don't do that. It's expensive, hallucination-prone, and the context limits will still bite you.
We use the AST and symbol graph to find the relevant snippets first. If we’re analyzing a SQL injection risk, we don't send the CSS files to the model.
We prune the graph to only include the data-flow path from the HTTP input to the DB query. Then, and only then, do we let the LLM look at the code. This reduces our token spend by 95% and increases accuracy because the model isn't distracted by boilerplate.
The 2-second rule
If an analysis takes longer than a git push, it's ignored. Developers don't wait for slow CI.
Our target is always sub-5 seconds for any repo under 100k lines. To get there, we had to stop thinking like application developers and start thinking like compiler engineers.
Stop using fancy abstractions. Get closer to the data. Keep things in the cache.
Building this wasn't about a single 'aha' moment. It was about removing a thousand small bottlenecks. If you're building code tools, start by looking at your memory allocations. That's usually where the performance is hiding.