Serverless Performance: Python vs Node.js in 2026

Listen to this article · 6 min listen

Your choice of a runtime for serverless functions isn’t just a style preference. It directly hits your wallet and your app’s responsiveness. Python and Node.js are the usual suspects because of their huge communities and libraries, but they behave very differently in a serverless environment like AWS Lambda. Knowing how they differ is how you get good performance without overspending.

Key Takeaways

  • Python cold starts are usually slower, often in the 150-300 ms range for a basic function, because its runtime is bigger and the interpreter needs to spin up, while Node.js might clock in at 80-150 ms.
  • Node.js is built for I/O-heavy jobs, using its non-blocking event loop to efficiently juggle tons of concurrent requests to things like databases or other APIs.
  • When you’re actually crunching numbers, Python can keep up with or even beat Node.js, especially when you bring in C-backed libraries like NumPy or use multiprocessing.
  • For both runtimes, keeping your packages small and picking the right memory setting is the fastest way to slash cold start times and your monthly bill.
  • You have to watch your functions. Tools like AWS CloudWatch or Azure Monitor give you the raw data you need to find the real bottlenecks and fix your configuration.

1. Baseline Performance Testing Setup

Don’t guess. Before you commit to an architecture, you need a solid baseline. We’ll stick to AWS Lambda here since it’s the market leader and its monitoring with CloudWatch is built-in. Let’s make two dead-simple Lambda functions, one using Python 3.11 and the other Node.js 20.x. All they’ll do is return a “Hello, World!” message. This lets us measure the raw runtime overhead without our own code getting in the way.

Python Function (lambda_function.py):

import json def lambda_handler(event, context): return { 'statusCode': 200, 'body': json.dumps('Hello from Python Lambda!') }

Node.js Function (index.js):

exports.handler = async (event) => { const response = { statusCode: 200, body: JSON.stringify('Hello from Node.js Lambda!'), }. Return response;
};

Set them both up with the exact same memory (start with 128 MB) and a 30-second timeout. You can deploy them through the AWS Management Console or the AWS CLI. If you’re using the console, just go to Lambda, hit “Create function,” pick “Author from scratch,” name it something like python-baseline-test or nodejs-baseline-test, select the runtime, and paste the code into the editor.

Pro Tip

Always start your functions at 128 MB. You’re billed on memory and execution duration, so giving a simple “Hello, World” function 512 MB of RAM is just burning money. You can (and should) tune it later if the function actually needs more power.

2. Measuring Cold Start Latency

Cold starts are the biggest headache in serverless. If your function’s been idle for a while (say, 15 minutes), the cloud provider tears down the old environment. The next time a request comes in, a new one has to be provisioned from scratch which means loading the runtime, your code, and all its dependencies. That load time adds user-facing latency.

To get a real measure of this, you have to invoke each function after it’s been sitting idle. The AWS CLI is good for this because it’s repeatable.

aws lambda invoke, function-name python-baseline-test, payload '{}' output.txt
aws lambda invoke, function-name nodejs-baseline-test, payload '{}' output.txt

After you run the command, jump into AWS CloudWatch and check the logs for that function. Find the line that starts with REPORT. In there, you’ll see Duration, Billed Duration, and the one we care about: Init Duration. That number is your cold start time, plain and simple.

Screenshot Description: A screenshot of AWS CloudWatch logs showing a Lambda function invocation. Highlighted are the “Duration” and “Init Duration” values within the REPORT line. For Python, expect Init Duration to be in the range of 150-300 ms, while Node.js might show 80-150 ms for a simple function.

Common Mistake

Don’t just run it once and call it a day. Cold start times are notoriously inconsistent because of things you can’t control, like where your function lands on Amazon’s physical hardware. To get a trustworthy number, you need to perform at least 10-15 cold invocations, spaced out over a few hours, and then average the Init Duration values.

3. Analyzing Execution Duration for I/O-Bound Workloads

The majority of what serverless functions do is wait. They wait for a database query, an API response from another service, or a file to be read from S3. This is what we mean by I/O-bound. This is an area where Node.js really shines because its event loop was designed for exactly this kind of waiting game, handling many operations without getting blocked.

Let’s test this by having our functions call an external API. The public JSONPlaceholder API is perfect for this.

Python I/O-Bound Function (lambda_function.py):

import json
import requests def lambda_handler(event, context): try: response = requests.get('https://jsonplaceholder.typicode.com/posts/1') response.raise_for_status() # Raise an exception for HTTP errors data = response.json() return { 'statusCode': 200, 'body': json.dumps({'message': 'Fetched data successfully', 'data_id': data['id']}) } except requests.exceptions.RequestException as e: return { 'statusCode': 500, 'body': json.dumps(f'Error fetching data: {str(e)}') }

Node.js I/O-Bound Function (index.js):

const fetch = require('node-fetch'); // Needs to be installed as a dependency exports.handler = async (event) => { try { const response = await fetch('https://jsonplaceholder.typicode.com/posts/1'). If (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(). Return { statusCode: 200, body: JSON.stringify({ message: 'Fetched data successfully', data_id: data.id }) }; } catch (error) { return { statusCode: 500, body: JSON.stringify(`Error fetching data: ${error.message}`) }; }
};

The Node.js function needs a dependency, node-fetch. So you’ll have to create a package.json and install it.

npm init -y
npm install node-fetch@2 # Use version 2 for common Lambda environments

After that, you’ll need to zip up your index.js file along with the node_modules directory and upload it to Lambda. Now, hammer both functions with invocations (maybe 50 times in a row) and check the average Duration in CloudWatch. You’ll probably find that the Node.js function consistently has a slightly lower average execution time for this kind of I/O task.

4. Evaluating CPU-Intensive Operations

Serverless isn’t just for quick API glue code. Sometimes you have jobs that actually need to do some heavy thinking: processing an image, transforming a large dataset, or running a financial model. When that’s the case, CPU speed is what matters most, and the choice between Python and Node.js gets a lot more interesting.

Let’s try a classic CPU-bound task: a recursive Fibonacci sequence calculation.

Python CPU-Bound Function (lambda_function.py):

import json def fibonacci(n): if n <= 1: return n else: return fibonacci(n-1) + fibonacci(n-2) def lambda_handler(event, context): num = int(event.get('number', 30)) # Calculate Fibonacci for 30 result = fibonacci(num) return { 'statusCode': 200, 'body': json.dumps({'message': f'Fibonacci({num}) is {result}'}) }

Node.js CPU-Bound Function (index.js):

function fibonacci(n) { if (n <= 1) { return n; } else { return fibonacci(n - 1) + fibonacci(n - 2); }
} exports.handler = async (event) => { const num = parseInt(event.number || 30, 10); // Calculate Fibonacci for 30 const result = fibonacci(num). Return { statusCode: 200, body: JSON.stringify({ message: `Fibonacci(${num}) is ${result}` }) };
};

To really make them sweat, invoke these functions with a payload like {"number": 35}. Watch the Duration in CloudWatch. You might be surprised to see Python holding its own or even running faster. The comparison isn't always obvious. Node.js runs on the highly optimized V8 JavaScript engine, but Python's ability to use C extensions under the hood for libraries like NumPy and SciPy gives it a serious advantage in scientific and numerical computing.

Pro Tip

For genuinely heavy CPU work in Python, stop messing with Lambda Layers and just use Lambda container images. This lets you bake all your complex C-extension libraries into a single deployable unit, which simplifies dependency management and can even help with cold starts by pre-packaging everything.

5. Optimizing Dependencies and Package Size

A huge chunk of your cold start time, especially with Python, is determined by your deployment package size. A bigger zip file takes longer for Lambda to download and unpack. It's that simple. For small functions, Node.js can have an edge here because its runtime is trim and its package culture encourages small, single-purpose modules.

For Python:

  1. Use Lambda Layers: Don't package common libraries like requests or boto3 with your function code every time. Put them in a Lambda Layer so they're separate and can be shared.
  2. Manual Tree Shaking: Be ruthless. If you only need one helper function from a massive library, see if you can pull out just that piece of code (license permitting) instead of importing the whole thing.
  3. Targeted Pip Installs: When you're building your package, use pip install, target /path/to/package. This puts the libraries right where they need to be and helps avoid including extra junk.

For Node.js:

  1. Prune Dependencies: Before you create your zip file, always run npm prune, production. This command strips out all your dev dependencies (like testing frameworks) and can dramatically shrink your node_modules folder.
  2. Use a Bundler: For anything non-trivial, a bundler like Webpack or Rollup is your best friend. It will bundle all your code into a single JS file and automatically perform "tree shaking" to remove any code that isn't actually being used.
  3. Layering: Just like with Python, you can and should put large, shared Node.js dependencies in Lambda Layers.

Screenshot Description: A screenshot of an AWS Lambda function's configuration page, specifically showing the "Layers" section. Illustrate how to add a custom layer containing common Python or Node.js dependencies.

Common Mistake

I see this all the time: people zip up their entire project folder, .git directory, documentation, and all. Don't do it. Clean your directory before you create the deployment package. Those few extra megabytes of test files or Git history can add tens of very real milliseconds to your cold starts.

6. Memory Allocation and CPU Throttling

In AWS Lambda, memory is CPU. When you configure your function's memory, you're also setting its CPU power. Doubling the memory from 256 MB to 512 MB effectively gives you twice the vCPU, a fact that's essential for tuning performance.

The best way to figure this out is with the AWS Lambda Power Tuning tool. It's a small serverless app you deploy to your own account that automates the process of finding the best memory setting. The tool itself is a Step Functions state machine that hammers your function with different memory configurations, then spits out a graph showing you the sweet spot where you get the best performance for the lowest cost.

Screenshot Description: A screenshot of the AWS Lambda Power Tuning results dashboard, showing a graph with "Cost" and "Duration" on the Y-axis and "Memory (MB)" on the X-axis. Highlight the optimal memory point where cost and duration are minimized.

You might find that a Python function doing some light processing runs best at 256 MB, while a similar Node.js function hits its stride at 192 MB. The goal isn't just to use less memory. It's to find the configuration that gives you the performance you need for the least amount of money, which is different for every function and runtime.

7. Monitoring and Iterative Refinement

You're never really "done" tuning. CloudWatch gives you the basics like invocation count, errors, and duration, but if you're serious about performance, you need to integrate a real Application Performance Monitoring (APM) tool built for serverless, like Datadog Serverless Monitoring or New Relic Serverless.

These tools provide distributed tracing, which lets you watch a single request as it flows through your system, from an API Gateway trigger, to a Lambda function, to a DynamoDB query, and back. They can show you precisely where the latency is coming from, whether it's a slow third-party API or an inefficient piece of your own code.

Check your metrics constantly. A high Throttles count means something is wrong with your concurrency settings or a downstream service can't keep up. If Duration is all over the place, you need to dig in and find out why. Sometimes the fix is a simple code change, like moving an expensive library import so it only happens inside a handler function when it's actually needed, which can make a huge difference for warm invocations.

So what's the verdict? For I/O-heavy apps that spend most of their time waiting, Node.js generally has an edge with faster cold starts and its asynchronous model. For CPU-bound tasks where you're crunching data, Python is often just as fast or faster, especially with its massive ecosystem of optimized scientific libraries. The biggest performance gains, however, won't come from your initial language choice. They'll come from being obsessive about your package size, using tools to find the right memory setting, and continuously monitoring your functions in production.

Are Python cold starts always worse than Node.js?

Most of the time, yes. The Python interpreter itself takes a moment to fire up, and its standard library is bulkier, which often leads to longer cold start times than Node.js. But you can shrink that gap a lot with smart packaging (like using smaller base container images) and putting dependencies in Lambda Layers.

Which language is better for handling lots of concurrent requests?

Node.js is usually the better choice for highly concurrent, I/O-bound functions. Its event-driven, non-blocking architecture is tailor-made for juggling thousands of simultaneous connections, like for an API gateway, without needing a separate thread for every single request.

Can I use Python for heavy computation in serverless?

Absolutely. Python is great for CPU-intensive work, especially when you use libraries like NumPy or Pandas that have performance-critical parts written in C. You just have to remember to give the function enough memory, since more memory also means more CPU power in Lambda. Managing multiprocessing is also an option, but it requires careful implementation.

How does memory choice affect performance for these languages?

In AWS Lambda, memory allocation is how you control CPU allocation. More memory equals more CPU power. This directly speeds up CPU-bound code in both Python and Node.js. It can also help with cold starts, as a function with more memory might get a more powerful CPU slice to initialize the runtime and your code faster.

Are there tools to help me pick between Python and Node.js?

Instead of guessing, use a tool like AWS Lambda Power Tuning. It's the best way to get a real answer for your specific code. It will run your actual function on both runtimes across a dozen different memory settings and give you a data-driven report on which configuration provides the best balance of speed and cost. That kind of empirical data is always better than a generic benchmark.

Kaito Nakamura

Senior Solutions Architect M.S. Computer Science, Stanford University; Certified Kubernetes Administrator (CKA)

Kaito Nakamura is a distinguished Senior Solutions Architect with 15 years of experience specializing in cloud-native application development and deployment strategies. He currently leads the Cloud Architecture team at Veridian Dynamics, having previously held senior engineering roles at NovaTech Solutions. Kaito is renowned for his expertise in optimizing CI/CD pipelines for large-scale microservices architectures. His seminal article, "Immutable Infrastructure for Scalable Services," published in the Journal of Distributed Systems, is a cornerstone reference in the field