6 min read

Building my first Rust project

Building my first Rust project

When I first joined Netlify, I worked on the Frameworks team which focuses on ensuring that all major web frameworks run out-of-the-box when deployed to the platform.

As part of our testing strategy at the time, we would make legitimate requests from test applications to test rewrite and redirect behaviour. This was something that we couldn't mock, and we needed to do it live in an end-to-end test.

Some of the URLs that we would make requests to were at the example.org and example.com domains. While they worked for some cases, it didn't cover all status codes (at least from what I recall) and when I noticed this I remember wanting to not be dependent on a third-party for this testing since it is rather simple to implement ourselves.

Fast forward to now and I thought this would be a great excuse to start learning Rust. So I made an application that returns whatever status code you request of it by using the final part of the path in the URL as the status code value you want.

I deployed this both as a Cloudflare Worker and as a Netlify Function to see the performance and resource usage of the tool.

Picking Rust for this project

Besides the learning opportunity that this provided, I've also decided to try and make any new project I build have as small of a carbon footprint as possible.

This starts with the choice of programming language being used.

A paper released in 2017 (and a follow-up in 2021) shows the differing energy consumption, execution times, and memory usage across languages:

Source: https://www.devsustainability.com/p/paper-notes-energy-efficiency-across-programming-languages

As noted in this blog post summarizing the findings, benchmarking this sort of thing can be tricky. But it's fair to say that, at scale, the differences found in the paper can add up to something meaningful.

Given Rust had one of the lowest scores in the tests and can also run on browsers as WebAssembly, I was keen to give it a try.

The results

Below are the main snippets of code from the project:

Cloudflare Worker code

src/utils.rs:

use cfg_if::cfg_if;

cfg_if! {
    // https://github.com/rustwasm/console_error_panic_hook#readme
    if #[cfg(feature = "console_error_panic_hook")] {
        extern crate console_error_panic_hook;
        pub use self::console_error_panic_hook::set_once as set_panic_hook;
    } else {
        #[inline]
        pub fn set_panic_hook() {}
    }
}

src/lib.rs:

use worker::*;

mod utils;

fn log_request(req: &Request) {
    console_log!(
        "{} - [{}], located at: {:?}, within: {}",
        Date::now().to_string(),
        req.path(),
        req.cf().coordinates().unwrap_or_default(),
        req.cf().region().unwrap_or("unknown region".into())
    );
}

#[event(fetch)]
pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result<Response> {
    log_request(&req);

    utils::set_panic_hook();

    let router = Router::new();

    router
        .get("/", |_, _| Response::ok("Hello from Workers!"))
        .get("/:status_code", |_, ctx| {
            if let Some(status) = ctx.param("status_code") {
                let status_code = status.parse::<u16>().unwrap_or(500);
                
                let res = Response::empty()?.with_status(status_code);
        
                return Ok(res);
            }
        
            return Response::error("Bad Request", 400)        
        })
        .run(req, env)
        .await
}

Netlify Function code

main.rs

use aws_lambda_events::event::apigw::{ApiGatewayProxyRequest, ApiGatewayProxyResponse};
use aws_lambda_events::encodings::Body;
use http::HeaderValue;
use http::header::HeaderMap;
use lambda_runtime::{handler_fn, Context, Error};
use log::LevelFilter;
use simple_logger::SimpleLogger;

#[tokio::main]
async fn main() -> Result<(), Error> {
    SimpleLogger::new().with_utc_timestamps().with_level(LevelFilter::Info).init().unwrap();

    let func = handler_fn(my_handler);
    lambda_runtime::run(func).await?;
    Ok(())
}

pub(crate) async fn my_handler(event: ApiGatewayProxyRequest, _ctx: Context) -> Result<ApiGatewayProxyResponse, Error> {
    let path = event.path.unwrap();
    let path_parts: Vec<&str> = path.split('/').collect();
    
    let status_code = match path_parts.last().and_then(|s| s.parse::<i64>().ok()) {
        Some(code) => code,
        None => 404,
    };
    
    let resp = ApiGatewayProxyResponse {
        status_code,
        headers: HeaderMap::new(),
        multi_value_headers: HeaderMap::new(),
        body: Some(Body::Text("Hello world!".to_owned())),
        is_base64_encoded: false,
    };

    Ok(resp)
}

This is ever-so-slightly out of date already as this follows the old convention/template Netlify had before I shipped some changes last week.

Resource usage and response times

The results I saw have me pretty excited overall but also very curious.

Starting with the exciting parts...

For the Netlify Function, the Rust version returns resource usages that look like the following in the logs:

Duration: 1.41 ms Memory Usage: 12 MB Init Duration: 15.96 ms

compared to the Javascript version:

Duration: 48.52 ms Memory Usage: 78 MB Init Duration: 197.62 ms

The response times that I was seeing from the browser for the Rust versions are blazing fast too - Cloudflare on average was returning <30ms responses.

Netlify was a little slower, coming in on average somewhere between the high 40s-low 60s milliseconds but I'm attributing this difference to Cloudflare Workers being run on the edge whereas Netlify Functions are not.

Now for the part that has me curious. I would've thought that the CPU time on Cloudflare Workers would be lower for the Rust worker instead of the Javascript version but instead, it was higher.

The Rust Cloudflare Worker had this for CPU time:

whereas the Javascript version has:

Not sure what the reasons explaining this difference are right now, but hopefully something I'll learn more about in the near future!

What I learned along the way

cargo lambda is your friend for Rust AWS Lambda functions

If you're looking to write an AWS Lambda function in Rust, cargo lambda can help you get up and running quickly, and provides sample payloads for you to test your function locally.

The lambda_http crate uses lambda_runtime under the hood

For the Netlify Function, one of the areas that I got a little tripped up on was whether to use the lambda_runtime crate or the lambda_http crate.

Initially, I tried to use the lambda_http crate so I could try and get fancy with this and use the Axum framework like in this example. When it wasn't working like I was hoping it would, a fellow engineer from Netlify theorized that it was because the Rust runtime, which is included in the lambda_runtime crate, was missing.

After digging into it further, I found out that lambda_http does use lambda_runtime under the hood (and so includes the Rust runtime), and the errors I was running into may have something to do with how I was writing the code and a mismatch of expected data types.

The Cache API isn't available on workers.dev domain in Cloudflare

Given there's no dynamic content being returned the user, I attempted to try and cache all the responses to make things even more performant.

However, I didn't realize that the Cache API isn't available on worker.dev domains and spent ages trying to figure out why this wasn't working. I finally came across this part in their documentation:

...any Cache API operations in the Cloudflare Workers dashboard editor, Playground previews, and any *.workers.dev deployments will have no impact.

which helped clear things up for me. Cloudflare has default TTL times for certain status codes, and because I had timeboxed this project I didn't try to find a workaround once I learned this.

Future things I'd like to look at

Investigating running a framework like Axum on a Netlify Function

I feel like this should be possible and I just need to work on the problem a bit more. I think making this work would unlock some cool opportunities for developers that use Netlify.

Measuring energy consumption of Javascript and Rust versions

I wanted to measure the energy consumption differences between a Javascript version and Rust, but unfortunately, I wasn't able to measure this before my self-imposed deadline for this.

My first impression of this though is that it looks like it'd be tricky to measure, especially when doing so for functions run on a cloud computing provider.

Differences in energy consumption by browser

I found this paper that compares the energy efficiency of WebAssembly and Javascript and what jumped out to me most in the overall findings was that the authors found a notable difference in energy consumption between the Chrome and Firefox browsers. I'd be curious to know what that difference translates to over time for the average internet user.


📫
Enjoy this post? Subscribe to be notified when I publish new content!