Sitemap

Rust vs. Python: Performance Insights from a Simple Backend Task

11 min readJan 8, 2024

All the code for both Rust and Python application can be found in this Github repository.

Introduction

When it comes to backend development, the programming language you choose can influence the outcome of your project, from implementation speed to final performance. I found myself in a position to compare the performance of two languages I frequently use: Python with the FastAPI framework, and Rust with Rocket. The project at hand was a basic one: fetching content from GitHub and comparing YAML files.

Python is a favorite among many languages for its straightforward syntax and the speed at which you can go from concept to code. FastAPI is a testament to this, providing a high-performance framework that’s easy to use. It’s a combination that has proven effective for quickly putting together backend services.

Rust is different. It’s designed with performance and safety in mind, often used in situations where every millisecond counts and memory management is critical. It’s known for its efficiency and the control it offers developers over low-level details.

But how do these languages stack up for everyday tasks? That’s what we set out to explore. We ran a set of tests to compare Rust and Python in a basic application scenario, using existing libraries, without reinventing the wheel. Our goal was to present results on how each language performs in terms of speed and resource usage without prescribing when to use one over the other.

In this article, we’ll share our findings from these tests. We’ll look at the raw performance data and give you a straightforward view of how Python with FastAPI and Rust with Rocket perform in a simple backend application. We acknowledge that the speed of implementation can vary widely among development teams, often influenced by their familiarity with the language and the complexity of the task, thus our focus here is on the runtime efficiency and resource utilization of the two applications.

While it is well known that Rust, a language designed for high performance or embedded software, is usually faster than Python, how big will the difference be on a pretty basic task ?

Keep reading as we break down the performance metrics and provide a clear picture of what we observed during our comparison.

The Goal: Comparing Python and Rust in a Practical Backend Task

The goal of our project was straightforward: compare the performance of two applications that accomplish the same task — one written in Python using FastAPI and the other in Rust with Rocket. We wanted to use the tools and libraries already available to us, avoiding the unnecessary work of building everything from the ground up. This approach mirrors a real-world scenario where developers often rely on existing frameworks and libraries to speed up development.

For our experiment, we containerized both applications using Docker. Each application interacted with its own MongoDB database, also running in a Docker container.

It’s generally expected that Rust would be more efficient in terms of processing speed and resource usage. But how much is more? Would the difference be significant enough to be a game-changer for such a “daily software engineering task”, or would Python hold its own in this race?

Keep in mind that choosing the right tool for a job can sometimes be counterintuitive — like finding that a train can be quicker than a plane for certain trips. In the same vein, picking between Rust and Python isn’t just about speed. It’s about the overall fit for the project, including factors like development speed, ease of maintenance, and the specific demands of the application.

In the next sections, we’ll outline our testing methods, share the performance data we gathered, and discuss what these results might mean for developers choosing between Python and Rust for their backend services.

The Application: Comparing Configurations Across Microservices

The task for our application was simple : comparing YAML configuration files across over a hundred microservices hosted in a GitHub repository. The goal was to assess the differences between two separate implementations of these microservices.

File System Schema

implementation_1/

├── microservice-1/
│ └── configuration.yml

├── microservice-2/
│ └── configuration.yml

├── ...

└── microservice-n/
└── configuration.yml

implementation_2/

├── microservice-1/
│ └── configuration.yml

├── microservice-2/
│ └── configuration.yml

├── ...

└── microservice-n/
└── configuration.yml

With the objective of comparing both the following files :

implementation_1/microservice_{i}/configuration.yml

implementation_2/microservice_{i}/configuration.yml

The process began by listing all the microservices within the first implementation. For each service, the application would then retrieve the configuration.yml file's contents.

In the Python version, we used the PyGithub library to facilitate communication with the GitHub API. For the Rust implementation, the Octocrab crate was our tool of choice. Both libraries served the same purpose and allowed us to maintain a similar logic flow in both versions of the application.

Pseudo Code Explanation of the Logic

function compare_configurations(implementation1, implementation2):
services = get_services(implementation1)
differences = []

for service in services:
config1 = fetch_configuration(service, implementation1)
config2 = fetch_configuration(service, implementation2)
if config1 and config2:
hashmap1 = yaml_to_hashmap(config1)
hashmap2 = yaml_to_hashmap(config2)
diff = deep_compare(hashmap1, hashmap2)
differences.append(diff)
save_comparison_to_database(service, diff)
return differences

The core of the comparison was a Depth-First Search (DFS) algorithm, which we used to methodically compare the YAML files after converting them into hashmaps. This approach allowed us to effectively identify differences and record them in the database associated with each application.

We tailored our application to compare the second implementation against the first, intentionally ignoring any services that were exclusive to the second. This decision was made to ensure a focused comparison, matching services directly between the two implementations.

In the following section, we’ll discuss the performance outcomes of our tests. We’ll examine how Python with FastAPI and Rust with Rocket performed in this specific backend task, providing a clear view of the results from our comparative study.

PyGithub vs. Octocrab : A Comparative Look

When it comes to interacting with the GitHub API, both Python and Rust offer robust libraries — PyGithub for Python and Octocrab for Rust. What about the nuances of these libraries, their coding styles, and implementation differences?

PyGithub

PyGithub is a Python library that enables developers to use the GitHub API v3 with Python. It provides a synchronous object-oriented approach to access the full range of GitHub API functions.

Example Usage of PyGithub:

from github import Github

# Initialize the client with an access token
g = Github("your_access_token")

# Fetch a repository
repo = g.get_repo("username/repository")

# Get contents of the configuration.yml file from a specific directory
contents = repo.get_contents("microservice-1/configuration.yml")

# Decode the content
config_data = contents.decoded_content.decode("utf-8")

PyGithub is known for its simplicity and ease of use. It allows Python developers to write scripts that interact with GitHub in a way that feels natural to the Pythonic style of coding—clear, readable, and concise.

Octocrab

Octocrab is a modern, extensible Rust crate for interacting with the GitHub API. It aims to provide a convenient way for Rust applications to communicate with GitHub, with a focus on asynchronous operations.

Example Usage of Octocrab:

use octocrab::{Octocrab, models};

// Create an instance of Octocrab
let octocrab = Octocrab::builder()
.personal_token("your_access_token".to_string())
.build()?;

// Fetch a repository
let repo = octocrab.repos("username", "repository").get().await?;

// Get contents of the configuration.yml file from a specific directory
let contents = octocrab
.repos("username", "repository")
.get_content()
.path("microservice-1/configuration.yml")
.send()
.await?;

// Decode the content
let config_data = String::from_utf8(contents.content)?;

Octocrab leverages Rust's powerful async/await features for non-blocking I/O operations, which can be particularly beneficial for applications that require high concurrency or have to deal with multiple API requests in parallel. For a fair comparison, we will use Octocrab the way PyGithub is implemented.

Performance Analysis: Comparing Backend Efficiency

To assess the performance of our Python and Rust applications, we set up a POST endpoint on both the FastAPI and Rocket servers. This endpoint received two implementation identifiers as input and calculated the differences in their configurations.
We conducted 10 runs of the comparison logic across more than 150 services and averaged the results to ensure a fair and more robust analysis.

Time Efficiency Results

The performance results were quite revealing. Rust completed the computation of all differences in just 59 seconds. Python, on the other hand, took an average of 2 minutes and 20 seconds to perform the same task.

This graph succinctly showcases the time efficiency of Rust compared to Python for this specific computation.

CPU and Memory Usage

We utilized the cAdvisor-Prometheus-Grafana suite to monitor the CPU and memory usage of our Docker containers. This allowed us to gather detailed metrics on the resource consumption of each backend.

CPU Usage Metrics

The CPU usage data indicated that the Python backend started with a eight times higher CPU usage baseline than Rust. Furthermore, when processing the differences, Python’s CPU usage (in green) spiked significantly, whereas Rust’s increase (in yellow) was much more moderate.

This visualization highlights the CPU usage patterns for both backends, with Rust showing a clear advantage in CPU efficiency.

Memory Usage Metrics

The memory usage trends were less pronounced to those of CPU usage.
Python consistently uses more memory than rust, even idle, due to several inherent characteristics (Runtime environment, Garbage Collection, GIL, …)
Rust consistently consumed less memory than Python (to such a scale we need two graphs for relevant visualizations — 10,8mb against 89mb), while both increased during the comparison operations.

This visualization highlights the Memory usage patterns for the Rust backend.
This visualization highlights the Memory usage patterns for the Python backend.

Primary Conclusion

Our primary performance analysis demonstrates that Rust’s advantages extend beyond high-performance applications only.
Through this pretty simple task, Rust showed greatly lower CPU and memory usage, and less than half of compute time.

Understanding Performance Differences: Language and Framework Effects

What could be the key factors contributing to the observed performance differences between our Rust and Python applications, particularly in the way each language handles dictionary comparisons and the impact of the web frameworks employed?

Rust vs. Python: Dictionary Comparisons

A factor in our application’s performance was the comparison of dictionaries (or Hashmaps). To focus on the languages’ performance without the additional layer of web server frameworks, we conducted a test comparing 150000 dictionaries in both Rust and Python.
Below is the averaged time a comparison took, in nanoseconds.

This visualization highlights the time for comparing two dictionaries in both languages.

While we observe rust is faster than Python, we are talking about a 852ns difference per pair of dictionaries (which would represent 0.1278ms for 150 pairs, out of 59s for the fastest implementation). This is not what made the difference for our use-case.

FastAPI vs. Rocket: Framework Impact

We also wanted to understand the impact of the web frameworks on our application’s performance. To do this, we created a basic endpoint in both FastAPI and Rocket that returned a simple JSON object. We then sent a million requests to each server locally at a rate of 100 requests per second with 50 workers.

Endpoint Performance Test

# FastAPI code for a basic JSON endpoint
from fastapi import FastAPI

app = FastAPI()
@app.get("/simple_json")
async def simple_json():
return {"message": "Python vs Rust"}
// Rocket code for a basic JSON endpoint
#[macro_use] extern crate rocket;

#[get("/simple_json")]
fn simple_json() -> Json<Value> {
Json(json!({"message": "Rust vs Python"}))
}
#[launch]
fn rocket() -> _ {
rocket::build().mount("/", routes![simple_json])
}

The results from this test would shed light on the performance of FastAPI and Rocket when handling a high volume of requests.

Results of the command hey -n 1000000 -m GET -q 100 http://localhost:<port>/simple_json

FastAPI averaged approximately 10ms in response time while Rocket divided this time by two with a strong 5ms.

This visualization highlights the average response time for a simple request with both frameworks.

In terms of CPU and Memory usage when load-testing both frameworks, we observed similar results as for the before mentioned computation.

This visualization highlights the CPU usage during load testing (1.000.000 requests) with both frameworks.

Octocrab vs. PyGithub: API Interaction

To compare the GitHub API interaction performance, we used Octocrab in Rust and PyGithub in Python to execute two simple get_content requests in a row (simulating both configurations to compare), averaging the time for 100 requests.
This comparison allowed us to evaluate the overhead each library adds to the application's performance when interacting with the GitHub API.
Once again, for a fair comparison, we will use Octocrab synchronously, the way PyGithub is implemented.

And the results we uncovered are enlightening : this is where our important difference for the whole computation takes place. Octocrab averaged 392ms per calls while PyGithub took 1740ms - which a more than one second difference per configurations comparison.

This visualization highlights the average response time for two get_contents requests with both libraries.

The significant performance difference between PyGithub and Octocrab, even when Octocrab is used synchronously, could be attributed to several factors inherent to the languages and libraries themselves, though dissecting the exact causes would be a topic for another article. Factors certainly go from language efficiency (concurrency model, memory management, ...) to system calls and I/O operations and HTTP client performance, with of course going through library implementation.

By conducting these focused tests, we could dissect the elements that contribute to the performance differences, offering a clearer understanding of the implications of language and framework choices in backend development.

Conclusion: A Closer Look at Performance Nuances

Our investigation into the performance of Rust and Python for a routine backend task has yielded some surprising insights. It’s quite remarkable to observe that, even when using some of the most popular libraries for tasks as common as fetching content from GitHub and comparing YAML files, the performance disparity between the two languages is notable.

However, it’s important to emphasize that the differences in raw speed between Rust and Python, and between Rocket and FastAPI, were relatively modest. The dictionary comparison times showed only an 850-nanoseconds advantage per comparison for Rust (less than 1ms for our 150 comparisons), and the response times for basic endpoints were 10 milliseconds for FastAPI versus 5 milliseconds for Rocket (this having almost no impact — we for example read in Designing Data-Intensive Applications that Amazon established that optimizing response time below 10ms had no more impact, while any ms over 100 had significant impact). These differences, while present, were not the primary contributors to the overall performance gap.

The most significant performance discrepancy arose from the libraries used to interact with GitHub. Despite using Octocrab in a synchronous manner to align with PyGithub’s behavior, the difference in execution time was substantial. Octocrab averaged 392 milliseconds per call, whereas PyGithub took a full 1740 milliseconds — resulting in more than a second’s difference for each configuration comparison. This was the key factor that led to the pronounced performance advantage seen in the Rust application.

In light of these findings, developers should consider performance in a holistic sense. The selection between Rust and Python should account for the entire development environment, including the libraries and tools that will be used. While Rust may offer superior performance in certain aspects (including its crates as shown during this article), Python’s ease of use and rapid development capabilities remain compelling for many projects. For this project, the Python implementation was completed in roughly half the time it took to implement the same functionality in Rust. However, this investment in development time can be easily justified, especially when considering the scale of the application and the potential for significant performance gains in the long run.

Ultimately, the decision to use Rust or Python should be informed by a combination of factors: the specific performance needs of the project, the development team’s proficiency with the language, the project’s timeline, and the overall development experience. Both languages have their place, and the best choice is one that aligns with the unique demands and goals of your project.

--

--

Responses (2)