Lambda Internals: Execution Model, Context Reuse & Cold-Start Root Causes
A Lambda function is not a stateless process that starts fresh on every call — it is a MicroVM that freezes between invocations and thaws on reuse. This article covers the three-phase lifecycle, execution context reuse, global scope caching, cold-start root causes, Provisioned Concurrency, and the memory-vCPU relationship that makes the counterintuitive 'more memory = cheaper' case real.
AWS Serverless Engineering: Lambda to Production
Lambda Internals: Execution Model, Context Reuse & Cold-Start Root Causes
Every mid-level engineer knows AWS Lambda runs their code when an event arrives. Every senior engineer knows Lambda runs inside a MicroVM with a three-phase lifecycle — Init, Invoke, Shutdown — and that the decisions you make about initialization placement, bundle size, and memory allocation determine whether your P99 latency is 12ms or 600ms at identical throughput. Every AWS primitive is a tradeoff surface, not a feature toggle. Cold starts are not random tax — they are a deterministic consequence of runtime initialization decisions you make before the first line of handler code executes. This article closes the gap between knowing that Lambda "spins up sometimes" and knowing exactly what happens in each phase, what you pay for, and which levers you control.
1. The Three-Phase Execution Lifecycle
Lambda does not run your handler directly. Every invocation travels through a three-phase lifecycle. Understanding which phase each line of your code belongs to is the most impactful mental model shift for serverless performance optimization.
1.1 Init Phase
The Init phase is the cold-start tax. It runs exactly once per MicroVM lifetime — not once per invocation. Lambda completes three sequential steps inside Init:
- Runtime bootstrap — The Lambda service allocates a MicroVM, downloads and unzips your deployment package (or pulls your container image layers), starts the language runtime (Node.js process, JVM, Python interpreter), and loads the runtime shim that translates Lambda's Invoke API into a handler call.
- Static initializer execution — Every statement in your module's top-level scope executes:
importstatements, constructor calls,constdeclarations that call functions. This is where SDK clients, database connections, and parsed configuration objects should live. - Handler registration — The runtime signals readiness to the Lambda service by sending a GET request to the Lambda Runtime API endpoint. The service acknowledges and the MicroVM enters a warm standby state.
Top-level await in ES modules is supported in Node.js 18+ Lambda runtimes. For CommonJS modules, use an IIFE or a module-level promise chain. Calling async initialization inside the handler on every invocation is the single most common Lambda performance anti-pattern.
1.2 Invoke Phase
Once Init completes, the Lambda service delivers the event payload to the registered handler. The Invoke phase runs on every invocation — warm or cold. Lambda passes two arguments:
event: The trigger payload, shaped by the event source (API Gateway, SQS, Kinesis, EventBridge, etc.)context: A runtime metadata object containingfunctionName,functionVersion,memoryLimitInMB,awsRequestId, and critically —getRemainingTimeInMillis()for timeout-aware processing
Use context.getRemainingTimeInMillis() as a progress gate inside batch processing loops. A handler that times out mid-batch returns no information about which records succeeded — partial batch response (SQSBatchResponse) reports only failures, enabling SQS to redeliver exactly those records.
1.3 Shutdown Phase
When Lambda decides to terminate a MicroVM — due to low demand, a deployment update, or the container reaching its maximum lifetime — it sends a SIGTERM signal before forcibly terminating. Your handler has a 300ms grace window to complete cleanup work.
Lambda Extensions receive a separate Shutdown lifecycle event with a configurable grace period (up to 2 seconds for external extensions). If you use a telemetry extension (e.g., AWS Distro for OpenTelemetry), it registers its own Shutdown hook independently of your SIGTERM handler.
2. Execution Context Reuse — The Freeze/Thaw Cycle
After Init completes and the handler finishes executing, Lambda does not destroy the MicroVM. Instead, it freezes the container in place — pausing all processes, preserving heap memory, open file descriptors, and network connections exactly as they were at handler exit. When the next invocation arrives for the same function, Lambda thaws the frozen container and delivers the event to the handler, bypassing Init entirely.
2.1 What Persists Across Invocations
The freeze/thaw cycle creates a category of state that neither exists in the event payload nor resets between invocations. Understanding this category prevents both bugs and missed optimizations:
| Persistent across invocations | Resets per invocation |
|---|---|
Module-level variables (const, let at top scope) |
event parameter |
| SDK client connection pools | context parameter |
| In-memory caches (Maps, Sets, arrays) | Handler local variables |
| Open file handles | try/catch state |
/tmp filesystem contents |
Stack frame |
| Environment variables |
Never call pool.end() inside the handler. This destroys the connection pool and forces re-initialization on the next warm invocation, defeating context reuse entirely. pool.end() belongs only in the SIGTERM handler.
2.2 The In-Memory Cache Pattern
Context reuse enables a lightweight caching layer with zero infrastructure cost — a module-level Map that lives for the MicroVM lifetime:
The module-level cache is a per-container cache, not a distributed cache. Ten concurrent invocations across ten MicroVMs each have their own independent cache instance. This is acceptable for configuration and small reference data — not for user session state or distributed locks.
3. Ephemeral Storage: /tmp
Every Lambda MicroVM has an ephemeral local filesystem at /tmp. Unlike in-memory variables, /tmp persists across freeze/thaw cycles and can be used to cache files — downloaded assets, compiled templates, ML model weights — that would be expensive to reconstruct on every warm invocation.
| Attribute | Value |
|---|---|
| Default size | 512 MB |
| Maximum size (configurable) | 10,240 MB (10 GB) |
| Persistence | Survives freeze/thaw within the same MicroVM lifetime |
| Sharing | Not shared between MicroVM instances |
| Encryption | At-rest encryption available (AWS KMS) |
For workloads requiring persistent shared storage across invocations and MicroVMs, mount Amazon EFS. EFS adds ~1–3ms latency per operation but enables shared state (ML model caches, shared file processing queues) across all concurrent Lambda instances.
4. Cold Start Root Causes & Mitigation
A cold start occurs when no warm MicroVM is available for a new invocation. Lambda must complete the full Init phase before executing the handler. Understanding what drives Init phase duration gives you precise levers to reduce it.
4.1 Init Phase Duration Breakdown
| Root cause | Typical contribution | Mitigation |
|---|---|---|
| Runtime bootstrap | 50–150ms (JVM: 300–800ms) | SnapStart (JVM), arm64 architecture |
| Dependency loading | 30–200ms | Tree-shaking, bundling, lazy requires |
| Static initializer execution | 20–400ms | Reduce global-scope work, defer non-critical init |
| Network calls in global scope | 50–500ms | Use Lambda Extensions for secret caching |
4.2 Bundle Size Impact
The single most controllable cold-start variable for Node.js runtimes is deployment package size. Lambda must download and unzip your package during Init. A naïve dependency tree that pulls the entire aws-sdk v2 monolith adds 4MB+ of unneeded code:
Use esbuild to bundle and tree-shake your deployment artifact:
Mark @aws-sdk/* as external when bundling for Lambda. The Lambda Node.js 18+ runtime includes the complete AWS SDK v3 — bundling it again adds 2–5MB to your package for zero benefit.
4.3 Provisioned Concurrency
Provisioned Concurrency keeps a configurable number of MicroVM instances perpetually initialized — they remain in the Init-complete state even when no invocations are pending.
Provisioned Concurrency is a reservation, not a guarantee for all traffic. If an API receives a sudden burst of 50 concurrent requests against a pool of 10 provisioned instances, the 11th through 50th requests all cold-start from On-Demand capacity. Provisioned Concurrency eliminates cold starts for steady-state baseline traffic — size it to your predictable P95 concurrency, not your burst peak.

5. Memory Allocation & the vCPU Relationship
Lambda allocates memory in 1MB increments from 128MB to 10,240MB. What most engineers miss: memory is the only vCPU lever available in Lambda. There is no separate CPU configuration.
| Memory | vCPU allocation | Network bandwidth |
|---|---|---|
| 128 MB | 0.08 vCPU | Low |
| 512 MB | 0.29 vCPU | Low |
| 1,024 MB | 0.58 vCPU | Moderate |
| 1,769 MB | 1.0 vCPU | High |
| 3,538 MB | 2.0 vCPU | High |
| 10,240 MB | 6.0 vCPU | Very High |
5.1 The "More Memory = Cheaper" Case
Lambda billing is invocations × GB-seconds. GB-seconds = (memoryMB / 1024) × durationSeconds. For CPU-bound functions, doubling memory can halve execution time — making the total GB-seconds lower at the higher memory tier:
Use AWS Lambda Power Tuning — an open-source Step Functions state machine that runs your function at multiple memory settings and produces cost-vs-performance curves:
Always benchmark with Power Tuning before hardcoding memory settings. The optimal memory setting is function-specific and depends on CPU-bound vs I/O-bound workload ratio. I/O-bound functions (waiting on DynamoDB, SQS) rarely benefit from memory above 512MB. CPU-bound functions (JSON transformation, image processing, crypto) often hit their cost minimum at 1,769MB (1 full vCPU).
5.2 SnapStart (Python & Managed Runtimes)
AWS Lambda SnapStart takes a snapshot of the initialized MicroVM state after Init completes and caches it in a tiered cache (memory → S3). Subsequent cold starts restore from the snapshot rather than running Init from scratch — reducing cold-start latency from 800–3000ms down to sub-second responses.
SnapStart snapshots the post-Init state. SDK initialization added after the snapshot is taken — inside the handler body, or in after_restore() — executes on every cold start and does NOT benefit from SnapStart. Move all heavy initialization to module level, before the snapshot checkpoint.

6. Practical Optimization Checklist
A senior serverless engineer applies this checklist to every new Lambda function before the first production deployment:
| Decision | Default (mid-level) | Correct (senior) |
|---|---|---|
| SDK client placement | Inside handler (every invocation) | Global scope (once per MicroVM) |
| Secret/config fetch | Inside handler | Global scope with TTL cache |
| Database connection | New connection per invocation | Connection pool in global scope |
| Bundle size | require('aws-sdk') |
@aws-sdk/client-* + esbuild |
| Memory setting | 128MB (default) | Benchmarked with Power Tuning |
| Cold-start strategy | None | Provisioned Concurrency for steady-state baseline |
| Python / JVM runtime | On-Demand (cold) | SnapStart |
| Cleanup | None | SIGTERM handler for connection draining |
Summary
| Concept | Rule |
|---|---|
| Global scope execution | Runs once per MicroVM Init, not per invocation — SDK clients and secrets belong here |
| Context reuse | MicroVM freezes after handler exits; thaws for next invocation — /tmp and in-memory caches persist |
| Cold start root causes | Runtime bootstrap + bundle size + static initializer duration — all three are controllable |
| SnapStart | Snapshots post-Init MicroVM; initialization inside handler defeats the cache |
| Memory and vCPU | Memory is the only vCPU lever; CPU-bound functions often cost less at higher memory |
| Provisioned Concurrency | Reserves N warm instances; burst beyond N still cold-starts |
What's Next
In Part 2: Lambda at Scale — Concurrency Architecture, ESM & Advanced Runtimes, we move from single-function optimization to system-level concurrency design: how the regional concurrency pool works, why one noisy function can throttle your entire account, how Kinesis ESM scales fundamentally differently from SQS ESM, and the BisectBatchOnFunctionError configuration that prevents a single malformed record from blocking a shard for days.
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.