Rust for AI: fast, safe inference, data tooling and where Python still wins
Rust rarely trains the model, but it increasingly runs it, tokenises its input, loads its weights and moves its data. This article maps the Rust AI ecosystem by job, shows three working patterns, and is honest about where Python is still the better tool.
The question behind the question
When someone asks "should we use Rust for AI?", they are usually asking something narrower. They have a model that works in a notebook, and something around it is too slow, too heavy or too fragile. The real question is whether Rust is the right tool for that part.
An AI system is not one program. It is a chain: data comes in, is cleaned and joined, becomes tensors, passes through a model, and the result is served. Each link has a different job and a different cost of failure.
Think of a restaurant. The recipe is invented in a test kitchen where people taste, change one thing and taste again. That is research and training, and it rewards speed of iteration. Dinner service is different: the same dish, hundreds of times a night, on time, with nobody getting ill. That is inference and data plumbing, and it rewards predictability and safety. Python owns the test kitchen. Rust is increasingly found on the line during service.
Why Rust shows up in AI systems
Memory safety without a garbage collector
Rust's compiler tracks who owns each piece of memory and how long every reference lives. Code that could read freed memory, or let two threads write the same data at once, usually does not compile. C and C++ are as fast but leave those checks to you. Go, Java and Python make memory safe with a garbage collector that periodically cleans up in the background.
Rust gets safety without that cleaner. Memory is freed at a known point, when its owner goes out of scope. In a shared office kitchen, one approach is a cleaner who clears the tables now and then, sometimes while you are still eating. Rust's approach is a rule that everyone washes their own cup before leaving.
Predictable latency
No collector means no collector pauses. For a model server this matters most at the tail: average latency is often fine in any language, but the slowest one per cent of requests is what breaks a service-level objective. It does not make the model faster. The matrix maths runs on the same kernels whichever language calls them.
Small binaries and fast start
A Rust service compiles to one native executable, with no interpreter or virtual environment beside it. Hugging Face states this as Candle's core goal: full frameworks such as PyTorch are very large, which makes creating instances on a cluster slow, and Candle aims for lightweight binaries and serverless inference. Text Embeddings Inference lists "small docker images and fast boot times" among its features.
WebAssembly and the edge
Rust compiles to WebAssembly (WASM), which runs in browsers and many edge runtimes. Candle ships WASM examples and in-browser demos of Whisper, LLaMA2, T5 and Segment Anything. Hugging Face tokenizers has a WASM binding on npm, and Burn supports WASM through its WebGPU and pure-Rust CPU backends.
A bridge to Python
PyO3 lets you write a Python module in Rust, and maturin builds it into a normal wheel. This is the quiet reason Rust is already everywhere in AI: the Python packages for tokenizers, safetensors and Polars are all Rust cores with PyO3 bindings. Many Python users run Rust daily without knowing it.
Where Rust fits, and where it does not
Rust is a strong fit when:
- The code runs many times and must not fall over: inference services, tokenisers, request-time features, ingestion workers.
- Latency budgets are tight, especially at the tail, or memory is limited.
- You ship where Python is absent: browsers, mobile and desktop apps, embedded devices, small containers.
- A Python pipeline has one hot loop that NumPy cannot vectorise.
- The work is data infrastructure: query engines, file formats, vector databases.
Rust is a poor fit when:
- You are still deciding what the model is. Research needs notebooks, plotting and every new paper's reference code, and that code is in Python.
- You are training large models. PyTorch, JAX and the distributed and fine-tuning tools around them live in Python. Candle and Burn can train, but the recipes and community knowledge are far thinner.
- The bottleneck is the GPU. If a request spends almost all its time inside a CUDA kernel, rewriting the wrapper changes little.
- The team does not know Rust and the deadline is close. In my experience the learning curve pays back on long-lived services, rarely on a two-week prototype.
Most teams should keep research and training in Python and use Rust selectively for the layers that run in production or at scale.
The ecosystem, organised by job
Figures come from each project's repository and the crates.io index at the time of writing.
Calling model APIs, agents and MCP
| Library | What it does |
|---|---|
| Rig | A library for LLM applications: one interface over many model providers and vector stores, agents with tools, streaming, and OpenTelemetry GenAI conventions. MIT. |
| rmcp | The official Rust SDK for the Model Context Protocol, built on tokio, with a macros crate for defining tools. Version 3.9.0, implementing the 2026-07-28 specification. Apache-2.0. |
| mistral.rs | A local inference engine that also offers an agentic loop and an MCP client, so a local model can call external tools. |
A common split is a Python agent calling a Rust MCP server: MCP is the contract, and each side uses the language that suits it. The agentic AI topic covers the agent side.
Serving and inference
| Library | What it does |
|---|---|
| Candle | Hugging Face's minimalist ML framework. PyTorch-like syntax; CPU, CUDA, Metal and WASM backends; loads safetensors, npz, ggml and PyTorch files; supports llama.cpp quantised types. Version 0.11.1. MIT or Apache-2.0. |
| ort | Rust bindings for Microsoft's ONNX Runtime, from pyke. Version 2.0.0-rc.15 wraps ONNX Runtime 1.31; 2.0 is still a release candidate. MIT or Apache-2.0. |
| tch-rs | Thin bindings to libtorch, the C++ API of PyTorch. Version 0.26.0; the README names libtorch v2.13.0 as the required version. |
| Burn | Tracel's tensor library and deep learning framework for training and inference, with CUDA, ROCm, Metal, Vulkan, WebGPU and CPU backends and ONNX import. MIT or Apache-2.0. |
| mistral.rs | A local LLM engine with a Rust crate, a Python package and a server exposing OpenAI- and Anthropic-compatible endpoints. Supports GGUF, in-situ quantisation, CUDA and Metal. MIT. |
| Text Embeddings Inference | Hugging Face's server for embedding, reranker and classification models: token-based dynamic batching, Candle and ONNX backends (via ort), HTTP and gRPC, OpenTelemetry and Prometheus. Apache-2.0. |
| Text Generation Inference | Hugging Face's LLM server. Its router, which queues requests and builds batches, is Rust; the model server behind it is Python over gRPC. Now in maintenance mode; its README recommends vLLM and SGLang instead. |
TGI is worth studying even as it winds down. It shows a proven shape: Rust at the front door doing networking, queueing and batching, with Python doing the maths behind it.
Vectors and retrieval
| Library | What it does |
|---|---|
| Qdrant | A vector search engine and database written in Rust, with payload filtering. Apache-2.0. Its qdrant-client crate is the Rust client. See Qdrant. |
| LanceDB | An embedded vector database on the Lance columnar format, with vector, full-text and SQL search. Rust core with Python and Node.js packages. Apache-2.0. |
Data processing
| Library | What it does |
|---|---|
| Polars | An analytical query engine written in Rust with DataFrame and SQL APIs, multi-threaded vectorised execution and spill to disk. Uses the Arrow format. MIT. See Polars. |
| arrow-rs | The native Rust implementation of Apache Arrow and Apache Parquet. Apache-2.0. |
| DataFusion | An extensible Apache query engine in Rust with SQL and DataFrame APIs, used to build databases and pipelines. Apache-2.0. |
Arrow is why these fit together. It is an agreed memory layout for columns of data, so Polars, DataFusion, DuckDB, LanceDB and pyarrow can pass data without converting it, like standard shipping containers that every port and lorry can handle without unpacking.
Classic ML and numerics
| Library | What it does |
|---|---|
| linfa | Classical ML in the spirit of scikit-learn: K-Means, GMM, DBSCAN, Naive Bayes, elastic net, random forest, ICA and more. MIT or Apache-2.0. |
| ndarray | N-dimensional arrays; the common currency between ort, linfa and rust-numpy. |
| rayon | Data parallelism: turns a sequential iterator into a parallel one with minimal changes. |
linfa suits a simple model embedded in a Rust service. It does not match scikit-learn's breadth; for gradient-boosted trees trained in Python, exporting to ONNX and running through ort is often more practical.
Tooling and glue
| Library | What it does |
|---|---|
| PyO3 | Rust bindings for Python, for writing native modules. Version 0.30. MIT or Apache-2.0. |
| maturin | Builds and publishes PyO3, cffi or uniffi crates as Python wheels. |
| rust-numpy | Bindings to the NumPy C API, so PyO3 functions can use NumPy arrays. |
| tokenizers | Hugging Face's tokenisation library: Rust reference implementation with Python, Node.js and WASM bindings. A v1.0 release candidate is in progress; the stable crate is 0.23.2. Apache-2.0. |
| safetensors | A tensor file format that cannot run code when loaded, unlike pickle, with zero-copy and lazy loading. Apache-2.0. |
| hf-hub | Downloads models and datasets from the Hugging Face Hub. |
| axum, tonic | A web framework on tokio, and a Rust implementation of gRPC. |
Three short code examples
These were checked against each project's current README and source.
A first tensor with Candle
This is Candle's own starting example, multiplying two random matrices on the CPU.
use candle_core::{Device, Tensor};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let device = Device::Cpu;
let a = Tensor::randn(0f32, 1., (2, 3), &device)?;
let b = Tensor::randn(0f32, 1., (3, 4), &device)?;
let c = a.matmul(&b)?;
println!("{c}");
Ok(())
}
With CUDA support, the only change is Device::new_cuda(0)?. Every operation returns a Result, so a shape mismatch is an error you handle, not a crash in a C++ library.
Running an ONNX model with ort
This uses ort = "=2.0.0-rc.15". Pin the exact version while 2.0 is a release candidate; in this candidate, sessions are created from an explicit environment.
use ort::{session::Session, value::Tensor};
fn main() -> ort::Result<()> {
let env = ort::init().build()?;
let mut session = Session::builder(&env)?.commit_from_file("model.onnx")?;
// One row with four features, shape [1, 4].
let input = Tensor::from_array(([1usize, 4], vec![5.1f32, 3.5, 1.4, 0.2]))?;
let outputs = session.run(ort::inputs![input])?;
let (shape, data) = outputs[0].try_extract_tensor::<f32>()?;
println!("shape {shape:?}, values {data:?}");
Ok(())
}
Note that run takes &mut self, so one session cannot serve two threads at once; use one session per worker. Also, the default features download a prebuilt ONNX Runtime at build time, which a locked-down build may want to replace.
A PyO3 function that releases the GIL
This module normalises a batch of embedding vectors. py.detach releases Python's global interpreter lock while Rust works, so other Python threads can run.
#[pyo3::pymodule]
mod fastvec {
use pyo3::prelude::*;
/// L2-normalise each row of a batch of vectors.
#[pyfunction]
fn l2_normalise(py: Python<'_>, rows: Vec<Vec<f32>>) -> Vec<Vec<f32>> {
py.detach(move || {
rows.into_iter()
.map(|mut r| {
let n = r.iter().map(|x| x * x).sum::<f32>().sqrt();
if n > 0.0 {
r.iter_mut().for_each(|x| *x /= n);
}
r
})
.collect()
})
}
}
With crate-type = ["cdylib"] and maturin develop --release, Python can import fastvec. Converting nested lists copies every element; for large arrays, rust-numpy works on NumPy memory directly.
Architecture patterns that work
Pattern 1: a Rust inference service
The model is trained in Python, exported to ONNX (or saved as safetensors for Candle) and served by a small Rust binary.
Flow: client → axum or tonic handler (validate, tokenise) → queue → batcher (up to N requests or T milliseconds) → worker threads, one ort session or Candle model each → response, with metrics and traces.
- Export the model and freeze its input and output names and shapes in a test.
- Load it once at start-up and fail fast if anything is wrong.
- Accept requests on tokio, but run inference on dedicated threads.
- Batch small requests; it is the main lever for GPU throughput.
- Expose health checks and metrics, and emit OpenTelemetry traces.
- Ship a small container and scale it as a stateless service.
This is roughly the shape of TEI, so for standard embedding models, use TEI. Write your own when the model or pre-processing is custom. For large LLMs, a dedicated engine will beat a hand-written server. The MLOps and AI infrastructure topic covers deployment.
Pattern 2: a PyO3 extension for one hot path
The pipeline stays in Python. One function, measured as the bottleneck, moves to Rust.
Flow: load → clean → hot loop → train or predict, where the hot loop becomes a call such as fastfeat.compute(...) into a Rust module built with PyO3 and maturin.
- Profile, and confirm no existing library already does the job.
- Give the Rust function a narrow interface: arrays in, arrays out.
- Release the GIL during heavy work; use rayon inside if it splits cleanly.
- Keep the Python version as a reference and test both give the same output.
- Build wheels per platform in CI so users only
pip install.
This is how tokenizers, safetensors and Polars reached Python users.
Pattern 3: edge and WASM inference
- Pick a small model, quantised if memory is tight.
- Build with Candle or Burn for
wasm32-unknown-unknown, or ship a native binary with ort for desktop and mobile. - Cache the weights; the download is usually slower than the inference.
- Keep a server fallback for slow devices.
You gain privacy, offline use and no per-request server cost, and pay with size limits and more device testing.
Performance and concurrency, explained simply
Where the time goes. A request has three parts: getting data in and out (networking, parsing, tokenising), the model's maths, and waiting. Rust helps a lot with the first and third, and little with the second when the maths already runs in an optimised kernel. Profile before rewriting.
Ownership makes threads safer. Rust marks which types can move between threads (Send) and which can be shared (Sync), and rejects code that shares mutable data without a lock. It is like a library where a book is lent to one person at a time, or read by many in the reading room but written in by none. Data races mostly become compile errors.
Async for waiting, threads for computing. Tokio handles thousands of mostly idle connections well. A model call is heavy blocking work, and if it runs on an async worker it stalls every request scheduled there. Use dedicated threads, tokio::task::spawn_blocking, or a channel to inference workers.
The GIL and the bridge. Python's global interpreter lock lets one thread run Python bytecode at a time. A PyO3 function that releases it lets Python continue while Rust uses all cores. But crossing the bridge copies data, so call Rust with large batches, not once per row.
Rust, Python and Go for AI work
| Concern | Python | Rust | Go |
|---|---|---|---|
| Research and training | The default | Possible, thin ecosystem | Rare |
| Production inference | Common (vLLM and framework servers) | Strong for lean services, edge, WASM | Good for API layers; maths via C bindings or a separate server |
| Memory management | Reference counting plus a collector | Ownership, no collector | Collector with low pauses |
| Tail latency control | Hardest | Most predictable | Good |
| Concurrency | GIL limits CPU threads | Threads and async, compiler-checked | Goroutines, very easy |
| Python interop | Native | Excellent via PyO3 | Less common |
| WASM and edge | Limited | Strong | Possible |
| Learning curve | Gentle | Steep | Gentle |
| Compile and iterate | Instant | Slower | Fast |
Python for the test kitchen, Go for the waiting staff and booking system, Rust for the parts of the kitchen that must run at full speed without accidents. Many real systems use all three.
A practical decision guide
- Is the model still changing every week? Stay in Python.
- Is there a standard server for your model type? Use it: vLLM for LLMs, TEI for embeddings, llama.cpp or mistral.rs locally.
- Is one Python function the bottleneck? Write a PyO3 extension for that function only.
- Must the model run where Python cannot? Use Candle, Burn or ort from Rust.
- Is the service latency-critical and long-lived? Use ort or Candle behind axum or tonic, and budget time for learning Rust.
- Is the job data infrastructure? Use the Rust engines already there, Polars, DataFusion, Arrow, Qdrant, LanceDB, from any language.
- Is the GPU the bottleneck? Change batching, model or quantisation first.
Common mistakes
- Rewriting before measuring, then finding the time was in the model kernel.
- Blocking the async runtime by running inference inside a tokio task.
- One ort session behind one lock for every thread, giving flat throughput.
- Not pinning release candidates. ort 2.0's API has changed between candidates.
- Copying across the Python bridge row by row instead of passing whole arrays.
- Breaking the export contract. Tokeniser, pre-processing and input names must match between Python training and Rust serving; test with real examples on both sides.
- Loading untrusted pickle files instead of safetensors.
Frequently asked questions
Can I train models in Rust?
Yes. Candle supports training, Burn is designed so the same code trains and serves a model, and tch-rs exposes libtorch's training features. What you lose is the surrounding ecosystem: recipes, distributed-training tools, experiment tracking and reference code for new papers. For most teams training stays in Python and Rust handles serving.
Is Rust faster than Python for inference?
For the model's maths, often not by much, because both call the same kernels. The gains come from everything around the model, such as parsing, tokenising, batching, networking and memory use, and from no interpreter or collector overhead. For small models, CPU inference and tight tail-latency targets the difference can be large; for a big LLM on a GPU, the engine's batching and kernels matter far more.
Should I use Candle or ort?
Use ort when you have, or can export, an ONNX model and want ONNX Runtime's optimisations and hardware execution providers. Use Candle for a pure-Rust stack, WASM targets, direct safetensors or GGUF loading, or a model already in candle-transformers. The export path you already have usually decides.
What is PyO3 and why does it matter for AI?
PyO3 lets you write Python modules in Rust, and maturin packages them as ordinary wheels. It lets Rust speed up Python without anyone changing language. Tokenizers, safetensors and Polars all reach Python users this way.
Is there an official Rust SDK for MCP?
Yes. The Model Context Protocol organisation maintains rmcp, built on tokio, with a macros crate for defining tools. At the time of writing it is at 3.9.0 and implements the 2026-07-28 specification while staying compatible with earlier releases. It suits MCP servers that wrap fast Rust code or need a single-binary deployment.
Is Text Generation Inference still a good choice?
Its README says it is in maintenance mode and recommends vLLM and SGLang for new deployments, with llama.cpp or MLX for local use. Its design, a Rust router before a Python model server, remains a useful reference. Text Embeddings Inference is a separate project and is still a strong choice for embeddings and rerankers.
Summary
Rust has found a clear place in AI systems: not in the notebook or the training loop, but in the layers that run every day and must be fast, safe and small. Hugging Face's tokenizers, safetensors, Candle and Text Embeddings Inference, ONNX Runtime through ort, data engines such as Polars, Arrow and DataFusion, vector stores such as Qdrant and LanceDB, and agent tooling such as Rig and the official rmcp SDK show how wide that place has become. Python remains the home of research and training because its ecosystem is unmatched. The practical approach is to keep Python where ideas change quickly, measure where time goes in production, and move only those parts to Rust: as a PyO3 extension, as a lean inference service, or as a model that runs on the edge.