top of page
  • Writer's pictureRan Isenberg

AWS Serverless Diversity: Multi-Language Strategies for Optimal Solutions

Go, Python and Rust Programming Languages
Go, Python and Rust Programming Languages

Serverless architecture offers both challenges and opportunities for optimization and efficiency.

In this post, you will learn from my experience about the challenges of developing a high-performance-oriented serverless service. I will share my insights into exploring multi-language strategies and see how embracing diversity in programming languages can lead to more robust solutions.

 

Table of Contents

 

Single AWS Lambda Language of Choice

I've always built serverless with Python. I've used Python AWS CDK code to create serverless resources and wrote my Lambda functions with Python.

Python is a natural fit for serverless development. It boasts a vast array of libraries, including Powertools for AWS and robust libraries for data engineers. Its versatility and excellent developer experience make it a top choice for serverless projects, offering a seamless and enjoyable development experience.

But hey, don't just take my word for it. Let's dive into some data that supports the superiority of Python in serverless development. 

DataDog's 'State of Serverless' report, a trusted industry resource, has revealed that Python and NodeJS are the undisputed champions in the realm of serverless development. 


https://www.datadoghq.com/state-of-serverless/
https://www.datadoghq.com/state-of-serverless/

For the past five years, Python has served my needs well and that's what my organization has mainly focused on. For every new service that we build, Python is the language of choice.

 

New Service, New Requirements

Lately, I've been working on a new service which has two types of APIs:

  1. Basic CRUD.

  2. Short runtime (single-digit millisecond if possible), maximum performance algorithmic calculation.


I'll use API Gateway to trigger Lambda functions. 

As for the Lambda function programming language, Python is a perfect match for the first CRUD requirement. The API has no special requirements and doesn't need to end in a single-digit millisecond.

However, as for the second one, some testing is needed as Python has yet to be known to achieve such performance.

 

My initial experiments showed that Python's numbers weren't great out of the gate, and the cold starts I was experiencing were a pain. There are ways to mitigate cold starts to some degree—check out my post about it here—but the warm Lambda performance was still not good enough. 


I didn't want to give up just yet. There are ways to squeeze some performance out of Python.


Squeeze Every Ounce of Performance

I've seen several approaches. Developers would dramatically increase Lambda's function memory, thus getting an improved CPU and lowering the total runtime but at an added cost.

Another approach I've seen is to utilize concurrency in the form of multi-threaded Lambda functions for cases that can benefit, such as multiple concurrent API or SDK calls. While it does work, it brings extra complexity. In addition, future requirements might increase the runtime and push the current design to its limit. We need to consider future requirements and have the option to extend the service.


All these solutions are valid, but they can only get you so far. Python has limits and performance boundaries, and to be honest, the solutions seem, in my eyes, to be a patch rather than a problem-solving method. The solutions might work well for now but will break with future added requirements.


The more I contemplated this matter, the less confident I was in my original design. 

I remembered a sentence I heard back in AWS re:invent 2023's Werner's keynote:


AWS re:invent 2023
AWS re:invent 2023

I can't tell you how many times I have heard this sentence from developers in my lifetime.

Just because we always do something the same way does not mean we should keep doing it. 

I realized that I was doing the same thing. I wanted to use Python again because that's how we've always done it. However, Python does not fit this API from a performance aspect, and every optimization is a patch that will likely fail when new requirements are introduced.


Something needed to change.

 

Serverless Multi-Language Diversity

If you recall my post about cold starts and optimizations, I quote AJ's tweet about cold starts and languages. Cold starts are foretelling regarding performance. 


Cold starts differences
Cold starts differences

Now, I'm not going to use C++ again; I left that chapter years ago, and it's not going to happen. C++ isn't memory safe and easy to use and would require extended time for developers to adapt. Rust is the new kid on the block, but I've heard mixed opinions about its developer experience, and there aren't many libraries around it yet. LLRT is too new for my taste, but Go caught my attention.

Go has a great balance between developer experience and performance; all the developers I know told me great things about it. It's simpler than Rust, provides good performance, has garbage collection, and my company has some experience with it. You can read more about Go and how it compares to Rust and Python here and here.


So Go it is. We'll need to verify the performance ourselves, but now we have a new, promising direction. However, we need to address two news issues:

  1. CRUD API - Do we change it to Go or write in Python?

  2. Short runtime API - Do we write its CDK code in GO or Python?


Lambda Function's Language Dilemma

The answer to the first question is simple. We'll use Python. There's no need to overcomplicate things. I can also keep using Python CDK in the CRUD project.

We'll split the service into two code repositories providing two smaller micro-services: 

  1. CRUD API - Python CDK & Python Lambda function.

  2. Short runtime API - Go Lambda function.


Having both the Python Lambda functions and the Go functions in the same project is possible. However, it brings unnecessary complexity. For example, your tests folder will contain Python and Go files, making a whole mess. As these functions implement different domains and have different requirements, it strengthens the case to split the service into two micro-services in two repositories: one with Lambda functions written in Python and the other in GO. 

 

Now, we need to address the second issue: What CDK language should we choose for the second micro-service, the one that contains Go functions.


CDK IaC Language - Single or Multiple?

While using a single language for both IaC scripts and Lambda handlers can streamline development and maintenance, embracing a multilingual approach can yield distinct advantages.

I discuss the advantages and the synergy between the CDK code and the Lambda code at my AWS re:invent session from 2023 below:



I've always used the same language for the IaC CDK definition and the Lambda function code. Managing dependencies is easier, but you can consider them as two separate projects in the same repository—one for the IaC and the other for the business domain code, the Lambda function.

As such, it might be ok to use two different languages.


For starters, why should I write CDK in Go anyway? CDK can be written in Go, but let's say we switch to Rust tomorrow. Should we change our CDK code to Rust - CDK has no Rust variant, so does that mean we can never use Rust? No, of course not; it does not make any sense. Besides, it makes no sense to rewrite the CDK construct from Python into Go just because the business domain is in Go. We can import existing Python constructs into the project and reuse code to accelerate our development. It makes little sense to forfeit these advantages because our Lambda function is written in Go. 


One of the key advantages of CDK Python is the familiarity our developers have with it. By sticking to Python, they can quickly write the CDK part, allowing them to focus on the business domain in Go and mitigate the overall risk and complexity of the service.

Ultimately, the business domain takes precedence over the CDK code. Regardless of the language we choose, CDK will build the necessary resources. The performance of go functions directly impacts the business, so it's crucial to direct our efforts where they matter most.


To summarize, Python CDK is fine and can be used to build Go Lambda functions or any other language.

AWS documentation is surprisingly excellent, and I created a CI/CD pipeline with CDK that compiles a Go executable and deploys an API Gateway within an hour, triggering a Go Lambda function.

You can follow this documentation and use the Lambda function template repo here.

 

Diversity in Serverless and Containers

We can take it even further. Dare I say, as a serverless hero, that Lambda functions may not be the best use case for my new service. Assuming large scale and constant traffic, Lambda functions may cost much more than their container counterparts. However, you need to consider the cost of maintainability, patching, security, and developer time and combine all factors to decide. In this post, I discuss FinOps and the importance of considering cost in the design stage. 

My service will initially use Lambda functions to get started and provide our API customers with value as soon as possible. However, going forward, it would be wise to consider containers with future scale and cost in mind.

 

My Final Take

Don't be afraid to try something new. 

As an architect, you must match the solution to the requirements and consider future changes. Don't choose a solution because that's how you always have done it. When you realize it doesn't fit your requirements, you must be brave enough to make a change. 

Diversity in serverless languages is an excellent example of this approach. 

You should choose Go or Rust for performance-oriented use cases and Python for the others (or Node.JS if you have experience). 

Lastly, mixing a Python CDK with a Go lambda in the same project is okay if you use AWS CDK. Your CDK language does NOT need to match the Lambda language.

Comentarios


bottom of page