Full rewrites are the default fantasy of every engineering team staring at a slow codebase. The logic goes: our Python monolith is slow, Rust is fast, let's rewrite everything. Lily Mara, a staff engineer at Discord and author of "Refactoring to Rust," spent her QCon talk dismantling that approach and proposing something more practical: rewriting individual functions in Rust and connecting them to your existing code through the C Foreign Function Interface.

Mara has been using Rust for over a decade, professionally since 2019. Her argument is not that Rust is slow or that rewrites are always wrong. It is that full rewrites carry enormous risk, blow past deadlines, and often reintroduce bugs that the original system already solved. Old code is not bad because it is old. It is complicated because it reflects years of accumulated real-world constraints that a rewrite from scratch tends to ignore.

Why full rewrites fail

The problems with full rewrites are well documented but worth repeating because teams keep making the same mistake. Deadlines slip. Scope expands. Institutional knowledge embedded in the original codebase gets lost. Bugs that were fixed months or years ago resurface because the new code does not account for the edge cases the old code already handled.

If your goal is performance, rewriting from scratch based solely on programming language choice ignores the rest of the stack. Database schemas, query patterns, caching layers, and microservice architecture all affect performance. Swapping Python for Rust at the application layer does nothing if your database is still doing full table scans on every request.

FFI refactoring at the function level

Mara's approach operates below the service boundary. Instead of breaking a monolith into microservices and rewriting each service in Rust, you identify the specific functions that consume the most CPU time and rewrite those functions alone. The rewritten function gets compiled into a native library, and your existing code calls it through the C Foreign Function Interface.

This works because C FFI is a lingua franca. Every mainstream operating system and programming language knows how to call a C function. The ecosystem already reflects this reality. NumPy and SciPy are built on Fortran code calling into Python through FFI. TensorFlow is written in C++ with bindings for multiple languages. OpenSSL is C code with bindings for nearly every language that exists. You are not inventing a new pattern. You are using one that has been proven at scale for decades.

For Python specifically, the PyO3 crate makes this integration straightforward. PyO3 generates wrapper code that turns a Rust module into something Python can import and call like any other module. Combined with a tool called Maturin from the same developers, you compile your Rust code into a library that Python loads at runtime. The Flask application continues running as before, but the hot path now executes native machine code instead of interpreted Python.

The tradeoffs are real

Mara is honest about what you give up when you go down this path. Deployments get more complicated. If your current deploy process is pushing Python files to a server and restarting a service, adding compiled native binaries means you now need to build those binaries for the correct operating system and architecture. Developer environments get harder to set up. You either install Rust toolchains on every developer machine, ship prebuilt native binaries that match each developer's system, or maintain two implementations of the same function, one in Rust and one in Python, for environments where the native build is not available.

There is also the possibility of introducing bugs. Mara argues this risk is lower when you are rewriting a single function than when you are rewriting an entire application, but it is not zero. The function you rewrite might have edge cases that are not obvious, or the FFI boundary might handle data types in ways that produce subtle differences in behavior.

Picking the right candidates

The strongest use cases fall into two categories. The first is operations that happen infrequently but are extremely expensive when they do. The second is operations that are cheap individually but run constantly across every request. Mara gives the example of request verification logic that sits in front of every API handler. It is not heavy on any single call, but if it accounts for 10% of your server runtime across all requests, cutting that to 1% or half a percent is a meaningful reduction in infrastructure cost at scale.

The language you are migrating from also matters. Mara plotted languages on two axes: the performance improvement Rust offers over the existing language, and the tooling support Rust has for FFI integration with that language. Ruby, Python, Node.js, and Lua sit in the sweet spot. They are significantly slower than Rust, and Rust has strong tooling support for generating callable libraries in each of those languages. Go is a worse candidate because its runtime makes C FFI expensive. Each call has to inflate the stack size, adding overhead that can negate the performance gain. C and C++ are fast but offer less improvement over Rust, though the memory safety guarantees Rust provides are the reason projects like the Linux kernel, Windows kernel, and Android are adopting it.

What this means for teams with legacy code

The practical takeaway is that you do not need to choose between maintaining a slow codebase and attempting a risky full rewrite. You can profile your application, identify the functions where Rust would have the biggest impact, rewrite those functions, and connect them to your existing code through FFI. The rest of the application stays in Python or Ruby or whatever language your team already knows.

This approach lets you deliver performance improvements incrementally, without rewriting your deployment pipeline, retraining your entire team, or risking the institutional knowledge embedded in your current code. It is not glamorous. It does not produce a blog post about rewriting your entire stack in Rust. But it gets the job done without the failure modes that doom most full rewrite projects.

For teams considering this path, Mara's talk is worth watching in full. The example walkthrough of rewriting a Flask statistics endpoint in Rust using PyO3 is concrete enough to replicate in your own codebase, and her assessment of which languages make good candidates for this kind of work will save you time evaluating whether the approach fits your situation.