10.2 Supported Runtimes: Python, Node.js, Java, Go, .NET, Ruby, Custom Runtime
Right, let’s talk runtimes. This is where the rubber meets the road, or more accurately, where your code meets Lambda’s execution environment. Think of a runtime as a pre-packaged, ready-to-go operating system for your function. It’s the layer of software that knows how to talk to the Lambda service, bootstrap your code, and crucially, how to execute it. AWS, in its infinite wisdom (and desire to get you locked in), provides a curated list of these for popular languages. We’ve got the usual suspects: Python, Node.js, Java, Go, .NET, Ruby. And then, for when you’re feeling particularly adventurous or masochistic, the “Custom Runtime” option. Let’s break them down.
The Managed Runtimes: Your On-Ramp
These are the ones AWS maintains for you. You pick a version (e.g., python3.9, nodejs18.x), you zip up your code, and you’re off. The beauty here is that you don’t have to think about the underlying Linux machine; you just get a clean, consistent environment every time.
But here’s the first “brilliant friend” tip: Always specify the full runtime identifier. Don’t just use python3. Use python3.9. Why? Because python3 is an alias that points to a specific version, and one day, AWS will update that alias from python3.9 to python3.11 and your function might break in spectacularly subtle ways because some underlying dependency changed. Be explicit. It’s the difference between “I’d like a coffee” and “I’d like a large black coffee from the Ethiopian Yirgacheffe beans you brewed at 8:03 AM.” One leaves room for error.
Each runtime has a specific, documented way it expects your code to be structured. It’s not magic; it’s just a contract.
# Python: Your handler function is in the format `filename.function_name`
# Save this as `lambda_function.py`
def lambda_handler(event, context):
# The `event` is the data passed to your function (e.g., a JSON from API Gateway)
# The `context` object is a treasure trove of info about the execution environment
print(f"Hello from Lambda! I see you sent: {event}")
return {
'statusCode': 200,
'body': 'I got your data. It was... interesting.'
}
// Node.js: Similar story, but note the async/callback patterns.
// You can use async handlers and just return a value now. It's much nicer.
exports.handler = async (event, context) => {
console.log('Received event:', JSON.stringify(event, null, 2));
return {
statusCode: 200,
body: `Hello, you're running on Node.js ${process.version}!`
};
};
The key thing to understand is that the runtime is responsible for calling your handler. It sets up the environment, parses the incoming event, and passes it to your function. When your function is done, the runtime takes whatever you return and packages it up to send back to the invoker.
The JVM Runtimes: A Different Beast
Java (and other JVM languages like Kotlin or Scala) work a bit differently. The cold start performance can be… noticeable. Why? The JVM has to startup, load your often-massive uber-JAR, and JIT-compile your code to get it running fast. The trick here is to do as much heavy lifting as possible outside the handler in static blocks or static variables. The runtime keeps the JVM instance around for subsequent invocations (warm starts), so that initialization cost is paid once.
// Java: Notice the use of static for the expensive operation.
// This runs once (per instance), not on every invocation.
public class HelloHandler implements RequestHandler<Map<String,String>, String> {
private static final SomeExpensiveObject EXPENSIVE_OBJECT = initializeExpensiveThing();
private static SomeExpensiveObject initializeExpensiveThing() {
// This runs during the initialization phase, not the invocation phase.
System.out.println("Initializing... This happens once per instance.");
return new SomeExpensiveObject();
}
public String handleRequest(Map<String,String> event, Context context) {
// This runs on every invocation.
return "Hello from Java! I'm already warmed up thanks to that static block.";
}
}
The Custom Runtime: Bring Your Own Everything
This is where you tell AWS, “Thanks, but I’ll handle it.” You provide the runtime. This is your escape hatch for using languages AWS doesn’t officially support (Rust, PHP, Elixir) or for using a very specific version of a language that AWS doesn’t provide.
But—and this is a big but—you are now signing up for system-level programming. You must provide a bootstrap file, an executable that acts as the entry point for your function. This bootstrap is responsible for starting your application, communicating with the Lambda Runtime API (an HTTP API inside the execution environment) to get events, and post responses back.
#!/bin/sh
# A simplistic bootstrap for a custom runtime
# This is the absolute bare minimum to illustrate the concept
set -euo pipefail
# Main loop: talk to the Runtime API
while true
do
# Get the next event
REQUEST_ID=$(curl -s -X GET "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next" -H "Lambda-Runtime-Trace-Id: tracingid" -D headers.txt | tee /tmp/event.json | jq -r .requestId)
# Extract the event data and request ID from the headers
EVENT_DATA=$(cat /tmp/event.json)
# Here you would invoke your actual application logic, passing the event
# For this example, we'll just pretend and create a response
RESPONSE='{"message": "Hello from a Custom Runtime!"}'
# Post the response back for that specific request
curl -s -X POST "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/response" -d "$RESPONSE"
done
The power is immense, but so is the responsibility. You’re on the hook for security patches, logging, and everything else. Use this when you have a very good reason.
The Golden Rule: Be Stateless (But Cache Like Hell)
This is the most critical concept. Every runtime shares this same execution model. An execution environment is potentially reused for multiple invocations. This is why you can have that static variable in Java or a global variable in Python that persists across calls. This is your superpower. Use it to cache database connections, HTTP clients, or parsed configuration outside your handler function. It’s the single biggest performance optimization you can make.
But remember: the environment is ultimately ephemeral. It can be frozen, thawed, or destroyed at any time. So while you should cache aggressively, never rely on the cache being there. Always code for a cold start. This duality—coding for both persistence and ephemerality—is the core mind-bend of serverless, and mastering it is what separates the novices from the pros.