10.1 Lambda Execution Model: Invocation, Execution Environment, Lifecycle
Right, let’s get into the engine room. You’ve got your function code, but how does AWS actually run it? The Lambda execution model is the secret sauce that makes this whole serverless thing work, and misunderstanding it is the number one cause of “but it works on my machine!” headaches. It’s not magic; it’s just a very clever, very disciplined system of recycling.
Think of it like a restaurant kitchen. AWS has a huge pool of chefs (execution environments). When an order comes in (an invocation), the head chef (the Lambda service) needs to find a chef for it. If a chef is already prepped and waiting, they just hand them the order. If not, they have to go hire a new chef, set up their station, and then hand them the order. That setup time? That’s your cold start.
The Three Phases of a Lambda Invocation
Every time your function is called, it goes through three distinct phases. You’re billed for the last two.
Init: This is the cold start phase. Lambda fires up an execution environment, which is essentially a microVM (think Firecracker) or container. It then downloads your code (the deployment package) and unpacks it. Finally, it runs any code you have outside of your handler function. This is huge, so let’s highlight it.
// Code outside the handler runs ONCE during Init (cold start) const expensiveDatabaseConnection = setupDatabaseConnection(); // <-- This happens during Init exports.handler = async (event) => { // Code inside the handler runs on EVERY invocation const results = await expensiveDatabaseConnection.query(event.query); return results; };Why is this brilliant? Because that costly database connection is reused across invocations, dramatically improving performance. You’re not paying to connect on every single call.
Invoke: Now the runtime enters the picture and finally calls your handler function, passing in the
eventandcontextobjects. This is the part you actually write logic for. The clock is ticking on your duration billing.Shutdown: After your function returns (or times out), the environment isn’t immediately destroyed. It’s frozen. Think of it as being put back in the fridge. For a short period (the length is an implementation detail AWS keeps close to its chest), the environment is kept around. If another invocation comes in, it’s thawed out and reused. This is a warm start. No Init phase, no downloading code, no running your outer scope code. Just straight to Invoke.
Execution Environment Lifespan
This is the most critical concept: an execution environment is not tied to a single request. It will be reused for multiple sequential invocations. This is why you must not assume state from a previous invocation.
Let’s look at a terrifyingly common mistake:
let count = 0; // This variable is in the Init phase scope!
exports.handler = (event) => {
count++; // đ¨ TERRIBLE IDEA. This will increment across invocations on a warm environment.
console.log(`Count is: ${count}`);
return 'Hello';
};
The first invocation on a cold environment logs Count is: 1. The next invocation on the same warm environment will log Count is: 2. This is a fantastic way to introduce bizarre, unpredictable bugs. Statefulness is your enemy in serverless. Embrace statelessness.
Concurrency and Scaling
You don’t manage servers; you manage concurrency. When a new request comes in and no warm environment is available, Lambda spins up a new one. It will do this as fast as it can, up to your account’s concurrency limit. Each concurrent execution requires its own environment. So if you have 100 requests hitting your function simultaneously, you’ll likely have 100 different execution environments, each with their own cold start.
This is why your function must be thread-safe. Never write to a location on the local filesystem without using a unique path (like ${os.tmpdir()}/${uuid.v4()}). Two different invocations can’t see each other’s memory, but they share the same read-only filesystem from your deployment package. If they both try to write to /tmp/output.log, you’re going to have a bad time.
Best Practices for the Model
- Optimize Your Init Phase: Put all your static initializationâSDK clients, database connections, loading large reference filesâoutside the handler. This turns cold start penalty into a long-term performance win.
- Assume Nothing is Warm: Write your code to be stateless. Use external services (DynamoDB, S3, ElastiCache) for persistence. Never rely on in-memory state or the
/tmpdirectory surviving. - Provisioned Concurrency is Your Cold Start Kill Switch: If you absolutely cannot tolerate cold starts for a certain function, use Provisioned Concurrency. This tells AWS to pre-warm and maintain a certain number of environments, so they’re always ready to go. It’s like reserving chefs, so you pay for them even when it’s quiet. Use it judiciously; it’s a premium feature.
The designers got this mostly right. The only truly questionable choice is the cryptic, almost passive-aggressive way it handles timeouts and out-of-memory errorsâyour function just vanishes without a trace mid-execution. Always set realistic timeouts and make sure your business logic is wrapped in try/catch blocks. Because when Lambda decides it’s done with you, it won’t say goodbye.