Posted on

Based on this comparison of different programming language's size and performance on Google Cloud Run, I decided to do the same test with some more languages. So in this post it's Rust.

Demo Rust Service

For this test of microservice deployment size and speed, I created a pretty identical service to Richard's example.

Here's my employee.rs file with a DTO struct for the employee data. Source code is here.

use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize)]
pub struct Employee {
  pub Id: String,
  pub FullName: String,
  pub Location: String,
  pub JobTitle: String,
}

And here are two functions in main.rs that serve the employee data, and run the web service.

async fn employees(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
  let v: Vec<employee::Employee> = vec![
    employee::Employee {
      Id: String::from("100"),
      FullName: String::from("Jack Donaghy"),
      JobTitle: String::from("Writer"),
      Location: String::from("NYC"),
    },
    employee::Employee {
      Id: String::from("101"),
      FullName: String::from("Liz Lemon"),
      JobTitle: String::from("Executive"),
      Location: String::from("NYC"),
    },
  ];

  Ok(Response::new(serde_json::to_string(&v).unwrap().into()))
}

Here's our main function with the listener configuration.

#[tokio::main]
async fn main() {
  let addr = SocketAddr::from(([0, 0, 0, 0], 8080));

  let make_svc = make_service_fn(|_conn| async { Ok::<_, Infallible>(service_fn(employees)) });

  let server = Server::bind(&addr).serve(make_svc);

  if let Err(e) = server.await {
    eprintln!("server error: {}", e);
  }
}

And that's it, after running cargo run locally this service spins up and answers requests for employee data on port 8080.

Build and deploy to Cloud Run

So now let's build a docker image and deploy to Cloud Run, see the file deploy_gcp.sh for details.

# Build and publish image to our cloud registry
gcloud builds submit --tag gcr.io/$1/rustservice

# Deploy image to Cloud Run and allow unauthenticated traffic to service
gcloud run deploy rustservice --image gcr.io/$1/rustservice /
  --platform managed --project $1 --region us-central1 /
  --allow-unauthenticated

Now we have our image published to our registry, at a pretty compact 10.9 MB using the Google's Distroless CC base image (extra compact just for running applications, the build is done using the full 1.6 GB Rust image).

We also already have our service deployed to cloud run, and can test the service with curl.

curl https://rustservice2-qtw3rvj3ya-uc.a.run.app | jq

And which returns our test data.

[
  {
    "Id": "100",
    "FullName": "Jack Donaghy",
    "Location": "NYC",
    "JobTitle": "Writer"
  },
  {
    "Id": "101",
    "FullName": "Liz Lemon",
    "Location": "NYC",
    "JobTitle": "Executive"
  }
]

Cold run

Now let's do the same performance test using hey that was done with the other frameworks.

hey -n 200 -c 10 https://rustservice2-qtw3rvj3ya-uc.a.run.app

Here are the cold start results (calling from a VM running in the same us-central1 (Iowa) region).

Summary: Total: 0.8240 secs Slowest: 0.6409 secs Fastest: 0.0051 secs Average: 0.0271 secs Requests/sec: 242.7116

Total data: 30600 bytes Size/request: 153 bytes

Response time histogram: 0.005 [1] | 0.069 [189] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 0.132 [0] | 0.196 [0] | 0.259 [0] | 0.323 [2] | 0.387 [6] |■ 0.450 [0] | 0.514 [0] | 0.577 [0] | 0.641 [2] |

Latency distribution: 10% in 0.0060 secs 25% in 0.0064 secs 50% in 0.0072 secs 75% in 0.0087 secs 90% in 0.0127 secs 95% in 0.3021 secs 99% in 0.6339 secs

Details (average, fastest, slowest): DNS+dialup: 0.0012 secs, 0.0051 secs, 0.6409 secs DNS-lookup: 0.0004 secs, 0.0000 secs, 0.0078 secs req write: 0.0000 secs, 0.0000 secs, 0.0012 secs resp wait: 0.0258 secs, 0.0050 secs, 0.6152 secs resp read: 0.0001 secs, 0.0000 secs, 0.0062 secs

Status code distribution: [200] 200 responses

Hot run

Now running again with the hot instances resulted in these numbers.

Summary: Total: 0.1737 secs Slowest: 0.0300 secs Fastest: 0.0059 secs Average: 0.0084 secs Requests/sec: 1151.3049

Total data: 30600 bytes Size/request: 153 bytes

Response time histogram: 0.006 [1] | 0.008 [177] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 0.011 [11] |■■ 0.013 [1] | 0.016 [0] | 0.018 [0] | 0.020 [0] | 0.023 [0] | 0.025 [0] | 0.028 [0] | 0.030 [10] |■■

Latency distribution: 10% in 0.0064 secs 25% in 0.0068 secs 50% in 0.0073 secs 75% in 0.0078 secs 90% in 0.0083 secs 95% in 0.0282 secs 99% in 0.0291 secs

Details (average, fastest, slowest): DNS+dialup: 0.0010 secs, 0.0059 secs, 0.0300 secs DNS-lookup: 0.0002 secs, 0.0000 secs, 0.0045 secs req write: 0.0000 secs, 0.0000 secs, 0.0003 secs resp wait: 0.0072 secs, 0.0058 secs, 0.0107 secs resp read: 0.0000 secs, 0.0000 secs, 0.0006 secs

Status code distribution: [200] 200 responses

Results summary

Here's a table of the 95% responses based on runtime & client location.

Call type Service running locally Service running on Cloud Run us-central1

Cold start 0.0009 secs 0.3021 secs

Hot start 0.0008 secs 0.0282 secs

And the full table with Dotnet, Go, Node, Java and Rust, averaging both cold and hot start 95% percentile results.

Language 95th percentile

dotnet 1.01s

go 0.33s

nodejs 1.32s

java 5.97s

rust 0.17s

And of course at the end, it all depends on which platform you are comfortable with and can use most effectively. But some basic performance expectations are of course important to consider. In a next step I would like to add Bun to this test, which uses a new Zig-based runtime for javascript.

Table of Contents