Rust for Python AI Infra
Most production AI agent stacks are written in Python, and most of them spend a surprising fraction of their wall-clock time somewhere Python was never good at: serializing JSON at the edges of a request, walking large token streams character by character, or holding a connection pool open under concurrent load. Neul Labs builds Rust-accelerated drop-in components for exactly that layer — fast-litellm and fast-langgraph are the two furthest along — and the pattern behind both is worth separating from the specific projects, because it applies to almost any Python AI service that has outgrown a single process.
The shape of the problem
A LiteLLM proxy or a LangGraph runtime is not slow because Python is slow at calling an LLM API — the network round-trip dominates that part of the request, and no amount of Rust changes a 400ms API call. The parts that benefit from a rewrite are the parts that run on every request regardless of which model answers it: request validation and routing, streaming token parsing, retry and fallback logic across providers, rate-limit bookkeeping, and the accounting that turns a stream of tokens into a cost and a usage record. These are CPU-bound, allocation-heavy, and — critically — embarrassingly parallel in a way the GIL actively fights.
Where a drop-in beats a rewrite
The instinct when a Python service gets slow is to rewrite the service. That is usually the wrong call for AI infrastructure specifically, for two reasons. First, the surrounding ecosystem — LangChain integrations, provider SDKs, eval frameworks, observability hooks — is overwhelmingly Python-first, and reimplementing all of it in Rust means either forking a moving target or falling behind it. Second, most of the actual hot path is a small, well-defined slice of the request lifecycle, not the whole service.
A drop-in keeps the Python surface area — the same functions, the same call sites, often the same import path — and replaces the implementation underneath with a Rust extension built through PyO3 or a similar binding layer. The proxy still looks like a Python service to everything that calls it. What changes is that the token-parsing loop, the retry state machine, and the request router now run as compiled, allocation-conscious code instead of interpreted Python with reference-counted objects on every step.
What actually gets rewritten
In practice, three components account for most of the benefit:
- Streaming response parsing. LLM providers stream tokens as server-sent events, and a naive Python parser re-decodes and re-allocates a string on every chunk. A Rust parser can operate on borrowed byte slices and only allocate once the response is reassembled for the caller.
- Multi-provider fallback routing. When a primary provider times out or rate-limits, the proxy needs to retry against a fallback within the same request, with backoff state tracked per-key. This is a small state machine that is trivial to get right in Rust and easy to get subtly wrong in Python under concurrent load, because backoff counters are exactly the kind of shared mutable state the GIL makes look safe until it isn’t. See LLM Provider Risk Management for the compliance side of running multiple providers behind one interface.
- Token accounting and rate limiting. Usage tracking has to run on every request without becoming the bottleneck itself. A Rust implementation backed by atomics or a lock-free structure holds up under far higher concurrency than a Python dict guarded by a lock.
A worked comparison
| Concern | Pure Python | Rust drop-in |
|---|---|---|
| Streaming token parse | Re-decodes per chunk, GC pressure under load | Borrowed-slice parsing, single final allocation |
| Multi-provider fallback | Shared state via GIL-guarded objects | Explicit state machine, no interpreter lock |
| Rate-limit bookkeeping | Dict + lock, contention under concurrency | Atomic counters, lock-free where possible |
| Ecosystem compatibility | Native | Preserved via PyO3 binding, same call sites |
| Rewrite surface | N/A | Narrow: the hot path only, not the whole service |
A checklist for deciding whether to reach for Rust
- Profile first. If the bottleneck is the network call to the model provider, no amount of Rust helps — fix the retry policy or the provider mix instead.
- Look for allocation-heavy code that runs on every request regardless of outcome: parsing, routing, accounting.
- Check whether the component has a narrow, stable interface. Components with a wide, frequently changing Python API are poor rewrite candidates; narrow internal utilities are good ones.
- Confirm the surrounding ecosystem dependency is one-directional — the Rust component should not need to call back into arbitrary Python business logic on the hot path.
- Ship the drop-in behind the exact same Python import path so callers do not need to change, and so the rollback is a version pin, not a refactor.
Limitations
This pattern is not a substitute for architectural fixes. If a service is slow because it makes three sequential LLM calls where one would do, or because it retries indefinitely against a provider that is genuinely down, Rust will make the same mistake faster. It also adds a build-and-packaging burden: a Python service that shipped as pure wheels now needs platform-specific compiled extensions, which complicates CI and constrains supported platforms unless that investment is made deliberately. And a Rust extension that panics can still take down the Python process hosting it if the binding layer does not catch and convert panics into Python exceptions — this has to be handled explicitly at the FFI boundary, not assumed.
FAQ
Does this mean the whole AI stack should move to Rust? No. The argument here is specifically for narrow, hot-path components inside an otherwise Python-first stack — not a rewrite of business logic, agent orchestration, or anything that changes shape often.
What binding layer makes this practical? PyO3 is the common choice for Rust-to-Python extensions: it handles the reference counting and type conversion at the boundary so the Rust code can present a normal-looking Python module.
Is this specific to LiteLLM and LangGraph? No — those are the two furthest-along Neul Labs projects, but the same reasoning applies to any Python AI service with a narrow, high-frequency hot path: proxies, gateways, and agent runtimes generally.
Bottom line
Rewriting an entire Python AI service in Rust is rarely worth it; the ecosystem gravity is too strong and the actual bottleneck is usually a small slice of the request path. Identifying that slice — streaming parse, fallback routing, rate-limit accounting — and replacing only it with a Rust extension behind the same Python interface gets most of the performance benefit with a fraction of the rewrite risk. See the Substrate Pattern for how the same “narrow, testable layer underneath” thinking applies to agent safety rather than performance, and Rust for AI Agent Infrastructure for the broader field guide this pattern comes from.
Related Articles
Rust for AI Agent Infrastructure
Why Rust dominates AI agent infrastructure. Memory safety, async runtime, the ecosystem, and the career path. A field guide from the founder of Neul Labs.
Building Developer Tools in the Open
Dipankar Sarkar shares lessons from 20 years of open source work, from Google Summer of Code to production developer tools that got adopted and sustained.
Early Open Source: Three Projects, 2005-2006
Dipankar Sarkar was one of the first five Indians in Google Summer of Code 2005, building WYSIWYG XUL editor for Mozilla and NFSv4 testing frameworks.