Our Tech: Rust

Why Rust?

As the team has changed over the last 5 years, our tech stack has undergone several iterations. Several years ago, as the first iteration, our backend systems were almost entirely written in Javascript and our infrastructure-as-code (IaC) was written in yaml. For the second development of our tech stack we switched to Typescript on both the backend and as our IaC in the shape of the AWS CDK library. This gave us a better developer experience for a number of reasons, with the main reason being the benefits of a type-system. Even with extensive testing Javascript code is liable to error at runtime, which is something Typescript can help with to a large extent. However, there are still some cases which Typescript does not cover. For example, because Typescript does not natively support pattern-matching, it is not possible to exhaustively pattern-match. This makes it harder to safely encode optionality and variants of data and guarantee runtime safety. These type of features are readily found in more mature type-systems, such as the one found in Rust, allowing us to write safer code more easily. This leads us to our third iteration of tech stack where we still use Typescript for our IaC and frontend, but have moved to Rust for our backend.

So in no particular order, here are some compelling benefits we’ve found from switching from Typescript to Rust.

🔥 Performance

Rust is renowned for blazing-fast performance, with extremely low memory overhead. This is something we’ve seen for ourselves. Our first benchmark was a function reading and writing to DynamoDB repeatedly. We found the Node version to be around 30% slower in clock time with 5x the memory usage.

The next thing we benchmarked was our ability to decode MBus frames (a standardised heat reading format) with an open-source Rust mbus library. Based on our benchmark we found that we could process 90 million Mbus frames within 15 minutes, fast enough to process every household in the UK comfortably before a football match’s halftime break is over.

Performance as a non-functional requirement is often a secondary consideration in software development, but it has real and tangible effects on end-users and directly affects their experience of our products. As such it is comforting to know that performance is baked-in to the language and we can expect high performance with minimal developer time spent on optimisation.

💷 Cost Savings

Related to performance is cost. We run a lot of our workloads on AWS Lambda, where cost is a function of memory and CPU utilisation. As a result, Rust gives us more opportunity to reduce operating expenses. By our calculations for a task with multiple reads and writes to DynamoDB, the equivalent Node lambda would require about 5x the memory to achieve the same performance results. Scaling this, on Lambda with the cost-explorer we found that a Rust lambda would cost 85% less than a Node lambda.

🛠️ Types

Although missing some features such as higher-kinded types, Rust has a fairly advanced type-system: algebraic data types (sum and product types), exhaustive pattern matching, parametric polymorphism, and ad-hoc polymorphism via the trait system. We can also apply more advanced type patterns: initial-encoding, phantom types, newtypes, and with generic associated types we can get a poor-person’s higher-kinded type. One of the main benefits of this type-system is that our ability to refactor has gone through the roof. Because of things like exhaustive pattern matching it becomes impossible for certain code paths to miss cases and return undefined behaviour. Small and large changes made to the code become almost trivial as the compiler keeps track of what’s left to change and what’s failing to conform. Moreover, because Rust utilizes the Hindley-Milner inference algorithm the cost of typing our code is low, lowering the barrier to entry and reducing another obstacle to refactoring.

We can’t talk about Rust and its type-system without talking about its use of an affine type-system - i.e. the borrow-checker. This is one of the ways Rust achieves phenomenal performance, as we have guarantees about the lifetimes of our variables without any runtime cost (no garbage collector and no runtime checks). That’s the benefit, but the cost of an affine type-system is increased complexity for the programmer (at least to begin with). However, we’ve found the Rust compiler to often offer useful suggestions when dealing with the borrow-checker, and overtime we’ve become more skilled at working with it.

🧪 Testing

Another benefit of Rust’s performance comes in the form of tests. Unit tests are fast. So fast, in fact, that we often find ourselves running the entire suite of unit tests locally, and not bother running individual unit tests. A suite of 100 unit tests in Node that took 2 seconds to run took only 0.1 second in Rust - a 20x improvement.

We also love the fact that Rust unit tests are written in the same file alongside the source code. This reduces context-switching and friction when writing tests as we don’t have to think about navigating the file-system or changing files when thinking about code and tests. This is such a simple change to the testing flow, but it’s a surprise more languages don’t allow this.

Conclusion

Overall, we’ve found Rust to help increase our confidence in the software we are producing for our heat network residents and clients. We look forward to producing more software with such strongly-typed languages and check out our next blogs to find out more of what we are working on and what techniques and tools we find useful.