How Lambda manages execution environments with Firecracker microVMs, handles cold starts, routes invocations, and scales from zero to thousands of concurrent executions.
45 min read2026-04-12hardlambdaawsserverlesshow-things-work
Interviewer: "Your team deployed a Lambda function that processes API Gateway requests.
Users are reporting intermittent 2-3 second latency spikes, but most requests complete
in under 100ms. The function code itself runs in 30ms. Walk me through what Lambda is
doing internally that causes these spikes, and how you would fix it."
This question tests whether you understand Lambda's execution model beyond "it runs your code." The interviewer wants to hear about cold starts, Firecracker microVMs, execution environment reuse, and the placement decisions that the Lambda control plane makes. If you just say "cold start," you get partial credit. If you explain the full lifecycle (download code, create microVM, initialize runtime, run init code, execute handler) and quantify where each millisecond goes, you nail it.
You: "Before I dive in, let me clarify a few things..."
"What runtime are we using? Cold start characteristics differ significantly between Python/Node.js (fast init) and Java/.NET (slow init with JVM/CLR startup)."
"How large is the deployment package? A 50 MB zip behaves very differently from a 5 MB one during the code download phase."
"Are we using VPC-attached Lambda? That used to add 10+ seconds of cold start for ENI attachment, though AWS fixed this with Hyperplane in 2019."
"What is the traffic pattern? Steady load, bursty, or periodic? This determines how often Lambda needs to create new execution environments."
"Are we using provisioned concurrency or relying on on-demand scaling?"
Why this matters: Lambda's internal behavior varies dramatically based on runtime, package size, VPC configuration, and traffic patterns. A candidate who asks these questions shows they understand that "cold start" is not a single number but a function of many variables.
Lambda runs your code inside Firecracker microVMs, lightweight virtual machines that boot in under 125ms and provide the same isolation as a full EC2 instance. When an invocation arrives, the Lambda Worker Manager checks if a warm execution environment exists for that function. If yes, it routes the request there (a "warm start," sub-millisecond overhead). If no, it triggers a : download the deployment package from S3, create a new Firecracker microVM, initialize the language runtime, run your init code, then execute the handler. This cold start takes 200ms to 2+ seconds depending on the runtime and package size. Lambda keeps execution environments alive for after the last invocation, reusing them for subsequent requests. Scaling follows a model: up to 500-3,000 concurrent environments instantly (region-dependent), then 500 additional per minute.
The architecture has three layers. The Frontend receives every invocation, authenticates it, checks concurrency limits, and passes it to the Worker Manager. The Worker Manager is the brain of Lambda. It tracks every active execution environment across the entire fleet and makes placement decisions in real time. The Worker Fleet consists of bare-metal EC2 instances running a component called the MicroManager, which orchestrates Firecracker microVMs on each host.
I find this architecture interesting because the Worker Manager maintains a global view of all execution environments. When your invocation arrives, it knows within milliseconds whether a warm environment exists and which worker host has capacity for a new one. This is what makes Lambda's scaling feel instantaneous.
The code storage layer deserves special mention. Your deployment package (zip file or container image) is stored in an internal S3 bucket, encrypted with your KMS key. When a cold start happens, the MicroManager downloads this package using a chunked, parallel download protocol that is significantly faster than a standard S3 GetObject call.
Lambda supports three invocation types, and the Frontend handles each differently:
Synchronous invocation (API Gateway, SDK invoke): The Frontend holds the HTTP connection open while the function executes and returns the response directly. If the function times out or errors, the caller gets the error immediately. No automatic retries.
Asynchronous invocation (S3 events, SNS, CloudWatch Events): The Frontend accepts the event, writes it to an internal queue, and returns 202 Accepted immediately. A separate component reads from the queue and invokes the function. If the function fails, Lambda retries twice with backoff (1 minute, then 2 minutes). After three failures, the event goes to a dead-letter queue or failure destination.
Event source mapping (SQS, Kinesis, DynamoDB Streams): Lambda's internal pollers pull records from the source and invoke the function synchronously. The polling, batching, and checkpointing logic is managed entirely by the Lambda service, not your code.
The invocation type determines your error handling strategy
For synchronous invocations, the caller is responsible for retries. For asynchronous invocations, Lambda retries automatically but you must configure a failure destination to avoid data loss. For event source mappings, Lambda manages retries per the source's semantics (SQS visibility timeout, Kinesis iterator position). Understanding which type your trigger uses is the first step in designing your error handling.
This is the foundation of Lambda's security model. Every execution environment runs inside a Firecracker microVM, not a container. This distinction matters enormously for multi-tenant security.
Firecracker is an open-source Virtual Machine Monitor (VMM) that AWS built specifically for serverless workloads. It uses KVM (the Linux kernel's built-in hypervisor) to create lightweight VMs that boot in under 125ms and consume as little as 5 MB of memory overhead. Compare this to a traditional VM that takes 30+ seconds to boot and consumes hundreds of megabytes of overhead.
Each microVM gets its own guest kernel, its own memory space, and its own network namespace. Unlike containers (which share the host kernel), Firecracker VMs have a true hardware isolation boundary enforced by the CPU's virtualization extensions (Intel VT-x / AMD-V). This means a malicious function in VM1 cannot attack VM2 even through kernel exploits, because they run on different kernels.
The key innovation of Firecracker is what it removes. A traditional VMM like QEMU emulates hundreds of devices (USB controllers, PCI buses, graphics cards). Firecracker emulates exactly five devices: a virtio-net network device, a virtio-block storage device, a serial console, a one-button keyboard (for shutdown), and a minimal clock. This stripped-down device model is what enables the sub-125ms boot time and the tiny 5 MB memory footprint.
Each Firecracker VM is also rate-limited at the network and storage level. The MicroManager applies cgroup-based rate limits to prevent one function from starving others on the same host. If your function tries to saturate the network adapter, the rate limiter throttles it without affecting other VMs. This is a critical density optimization: AWS can pack thousands of microVMs on a single bare-metal host because each one is constrained.
Why Firecracker, not containers?
Lambda originally launched using container-based isolation. AWS switched to Firecracker because containers share the host kernel, and kernel vulnerabilities (like Dirty COW or various eBPF exploits) could allow a tenant to escape their isolation. With Firecracker, each function gets its own kernel, so a kernel exploit only compromises that one execution environment. This is the same isolation model as EC2, but with 100x less overhead.
The memory model is worth understanding. Each microVM is allocated exactly the memory you configure for your Lambda function (128 MB to 10 GB). CPU is allocated proportionally: a 1,769 MB function gets one full vCPU. Below that, you get a fractional vCPU. This is why increasing memory also speeds up CPU-bound functions.
The cold start is the most discussed aspect of Lambda, and the most misunderstood. I will break down exactly what happens during a cold start, with timing for each phase.
Here is where the time actually goes:
Phase 1: Placement Decision (1-2ms): The Worker Manager looks up whether a warm execution environment exists. If not, it selects a worker host with available capacity. This is fast because the Worker Manager maintains an in-memory map of all environments.
Phase 2: Code Download (50-200ms): The MicroManager on the selected worker downloads your deployment package from Lambda's internal S3. AWS uses a chunked download protocol and caches packages on worker hosts, so subsequent cold starts for the same function version on the same host skip this step. Container images use a lazy-loading technique where only the blocks actually needed are fetched, reducing effective download time.
Phase 3: MicroVM Boot (approximately 125ms): Firecracker creates a new VM with the allocated memory, attaches a minimal guest kernel, and boots it. This is remarkably fast because Firecracker uses a stripped-down kernel with only the necessary drivers and no unnecessary services.
You can identify cold starts in CloudWatch Logs by the Init Duration field that appears only on cold start invocations. A typical log line looks like:
REPORT RequestId: abc-123
Duration: 30.42 ms
Billed Duration: 31 ms
Memory Size: 256 MB
Max Memory Used: 89 MB
Init Duration: 312.56 ms
The Init Duration of 312.56ms covers phases 3-5 (VM boot + runtime init + your init code). Phases 1-2 (placement + code download) are not included in this metric because they happen before the execution environment exists. The total cold start latency perceived by the caller is Init Duration + code download time + placement time.
For detailed cold start analysis, I recommend enabling Lambda Insights (a CloudWatch extension) which breaks down init duration into Runtime initialization and Function initialization subphases. This tells you exactly how much of the cold start is your code versus the runtime.
AWS SnapStart (launched in 2022) is a game-changer for Java Lambda functions. Instead of booting the JVM from scratch on every cold start, SnapStart takes a Firecracker snapshot of the fully initialized execution environment (after your init code runs) and stores it encrypted.
On a cold start, instead of the full 5-phase process, Lambda restores the snapshot in under 200ms. The JVM heap, loaded classes, JIT-compiled code, and your init state are all restored from the snapshot. This reduces Java cold starts from 2-3 seconds to 200-400ms.
There is a catch: any state that depends on uniqueness (random seeds, connection handles, ephemeral tokens) must be refreshed after snapshot restoration. AWS provides a hook (beforeCheckpoint / afterRestore) for this. If your init code opens a database connection, that connection will be dead after restoration because the server-side socket is gone. You must re-establish it in the afterRestore hook.
Lambda supports container images up to 10 GB, but this is a packaging format, not a runtime format. Your container image is deployed to Lambda's internal ECR, and when a cold start happens, Lambda uses lazy loading to fetch only the blocks of the image that the runtime actually reads.
This means a 5 GB container image does not take 10x longer to cold start than a 500 MB zip. Lambda's block-level loading pulls only the layers and files accessed during init. In practice, container image cold starts are comparable to zip deployments for the same runtime, because most of the image (build tools, intermediate files, unused dependencies) is never read.
The real advantage of container images is control. You can install custom native libraries (FFmpeg, ImageMagick, machine learning frameworks), use any base OS (Amazon Linux, Debian, Alpine), and test locally with identical tooling (docker build, docker run). The trade-off is larger deployment artifacts and a dependency on ECR.
Phase 4: Runtime Initialization (5ms to 1+ second): This varies enormously by language. Node.js and Python initialize in under 10ms. Go compiles to a native binary, so there is no runtime initialization. Java and .NET require JVM/CLR startup, class loading, and JIT compilation, easily taking 500ms to 2 seconds.
Phase 5: Init Code Execution (your code, highly variable): This is the code outside your handler function. It includes importing modules, establishing database connections, loading configuration. This runs once per execution environment and is the phase you have the most control over.
Init code runs during cold start, but is billed differently
In December 2023, AWS started billing for the init phase (up to 10 seconds) at the same rate as handler execution. Before this change, the first 10 seconds of init were free. This means your cold start init code now directly impacts your Lambda bill. Keep imports minimal and defer heavy initialization to the first handler invocation if possible.
The /tmp directory persists across warm invocations and can store up to 10 GB (configurable at function creation). This makes it useful for caching downloaded files, compiled assets, or ML model weights. On the first invocation, download the model from S3 to /tmp. On subsequent warm invocations, check if the file exists in /tmp and skip the download.
I have seen this pattern reduce average invocation latency by 80% for functions that load large reference data. The trade-off is that /tmp is not guaranteed to persist (environment can be reclaimed), so you must always handle the cache-miss case.
Beyond /tmp, any global variable (module-level in Python, package-level in Go, static in Java) persists across warm invocations. This is how connection pooling works: you create the database connection at module level, and every warm invocation reuses it.
import boto3# Created once during init, reused across warm invocationsdynamodb = boto3.resource('dynamodb')table = dynamodb.Table('Users')def handler(event, context): # This reuses the existing connection and session response = table.get_item(Key={'userId': event['userId']}) return response['Item']
This pattern applies to HTTP clients, SDK clients, decrypted secrets, parsed configuration files, and any expensive initialization. The rule of thumb: if it costs more than 1ms to create and does not change between invocations, initialize it globally.
This deserves emphasis because it is the most fundamental difference between Lambda and a traditional web server. Each Lambda execution environment processes exactly one invocation at a time. While your handler is running, no other invocation can enter that environment.
This has profound implications:
No concurrency bugs: Your handler never runs concurrently with another invocation in the same environment. Global variables are safe to read and write without locks.
No shared state between concurrent requests: Two simultaneous requests to the same function run in completely separate environments with independent memory, file systems, and connections.
High environment count: A service handling 1,000 concurrent requests needs 1,000 execution environments. If each takes 100ms, you need 100 environments for 1,000 RPS.
Connection explosion: Each environment opens its own database connection, potentially creating thousands of connections to your database during traffic spikes.
This model is the exact opposite of Node.js, Go, or Java web servers, which handle thousands of concurrent requests in a single process. Understanding this difference is essential for capacity planning and cost estimation.
After a cold start, Lambda keeps the execution environment alive and reuses it for subsequent invocations. This is how most invocations achieve sub-millisecond routing overhead.
The Worker Manager is fundamentally a distributed state machine. It tracks:
Environment registry: A map of function ID to list of active execution environments (warm/busy/initializing).
Worker capacity map: Available memory and CPU on each worker host in the fleet.
Code cache index: Which worker hosts have which function versions cached (to minimize code download time).
Concurrency counters: Per-function and per-account concurrent execution counts.
This state must be consistent enough to make correct placement decisions at thousands of requests per second. AWS likely uses a combination of in-memory caches (for fast lookups) and eventual consistency (for cross-region state). The Worker Manager does not need perfect accuracy: routing an invocation to a worker that recently lost capacity is handled by a retry (the MicroManager on that worker will reject the placement, and the Worker Manager will try another host).
Your global variables, imports, and connections survive
The /tmp directory (up to 10 GB) retains files
Here is the scaling timeline for a function that needs to reach 10,000 concurrent executions:
The key insight is that burst scaling is fast but limited, and linear scaling is unlimited but slow. For workloads that need sustained high concurrency, you need to plan the ramp-up time or use provisioned concurrency to pre-warm the environments.
When a new invocation arrives for the same function version, the Worker Manager routes it to an existing warm environment. The routing adds roughly 1ms of overhead, and your handler executes immediately without any initialization.
Connection reuse is the biggest warm start benefit
Database connections, HTTP clients, and SDK clients initialized in your init code persist across invocations. This is why you should create your database connection pool and SDK clients outside the handler function. A warm invoke reuses the existing TCP connection to your database, avoiding the 20-50ms connection setup overhead.
The idle timeout is not documented precisely, but empirical testing shows execution environments stay alive for 5 to 15 minutes after the last invocation. The exact duration varies and is managed dynamically by the Worker Manager based on fleet utilization. During high-demand periods, environments may be reclaimed sooner.
There are important constraints on warm starts:
One invocation at a time: Each execution environment handles exactly one invocation at a time. If 10 requests arrive simultaneously, Lambda needs 10 separate environments. This is fundamentally different from a container running a web server that handles concurrent requests.
No cross-version reuse: If you deploy a new function version, existing warm environments are not reused. New invocations to the latest version trigger cold starts. Old environments drain (finish their current invocation) and are terminated.
No cross-account reuse: Execution environments are never shared across AWS accounts, even on the same worker host. This is enforced by Firecracker's VM isolation.
The Worker Manager is the most critical (and least discussed) component of Lambda. It maintains a real-time map of every execution environment across thousands of worker hosts and makes sub-millisecond placement decisions.
When an invocation arrives, the Worker Manager follows this decision tree:
Check for warm environment: Look up the function ID in the environment registry. If a warm, idle environment exists, route there. This is the fast path (less than 2ms).
Check for pre-warmed environment (provisioned concurrency): If the function has provisioned concurrency configured, a pool of pre-initialized environments is always available. Route to one of these.
SQS event source mapping scaling deserves deeper explanation because it surprises many teams. When Lambda first starts polling an SQS queue, it creates 5 concurrent pollers. If messages are available, it scales up to 60 long-polling connections within a minute. Then it adds 60 more connections per minute until it reaches the concurrency limit or the queue is drained.
This means a sudden spike of 100,000 SQS messages does not immediately trigger 1,000 Lambda invocations. Lambda ramps up gradually: 5 at t=0, 60 at t=1min, 120 at t=2min, and so on. If you need faster processing of SQS spikes, reduce the batch size (to process more messages per polling round) or increase the function's reserved concurrency.
One subtle detail: Lambda deletes SQS messages using DeleteMessageBatch only after your function returns success. This means your function's execution time directly impacts throughput. A function that takes 5 seconds to process a batch of 10 messages provides 2 batches/second per poller. Reducing processing time to 500ms gives you 20 batches/second per poller, a 10x throughput improvement.
Entire message group blocks until retry succeeds or goes to DLQ
Blocks in-order processing
Strict per message group
Kinesis
Shard blocked, retries from failed batch
Retries forever (blocks shard) unless you configure max retry or bisect
Strict per shard
DynamoDB Streams
Same as Kinesis
Same as Kinesis
Strict per shard
The Kinesis/DynamoDB behavior is especially dangerous. A single "poison pill" record (one that always causes your function to fail) blocks the entire shard indefinitely. All subsequent records in that shard queue up behind it. This is why bisectBatchOnFunctionError and maxRetryAttempts with a failure destination are essential for production Kinesis consumers.
Lambda Extensions (launched in 2020) allow you to run additional processes alongside your function in the same execution environment. Extensions register with the Lambda Telemetry API and receive lifecycle events (INIT, INVOKE, SHUTDOWN) and telemetry data (logs, metrics, traces) without modifying your function code.
Common extension use cases:
Monitoring agents: Datadog, New Relic, and Dynatrace agents run as extensions, collecting metrics and traces without code changes.
Secret management: Extensions can fetch secrets from AWS Secrets Manager during INIT and cache them for the function.
Log forwarding: Extensions receive log output via the Telemetry API and forward it to external systems before the environment shuts down.
Extensions share the execution environment's memory and CPU. A Datadog extension consuming 50 MB of memory means your function has 50 MB less. Account for this when sizing your function's memory.
3. Cold start placement: If no warm environment exists, select a worker host. The selection considers: available memory on the host, AZ distribution (spread across AZs for resilience), whether the host already has the function's code cached (reduces code download time), and current host CPU utilization.
Burst check: If the function is already at its burst concurrency limit, the invocation is throttled (returns a 429 TooManyRequestsException for synchronous invocations, or gets retried for asynchronous invocations).
Throttling is per-function AND per-account
Lambda has two concurrency limits. The account-level limit defaults to 1,000 concurrent executions across all functions in a region (adjustable up to tens of thousands). The function-level reserved concurrency carves out a dedicated pool from the account limit. If you set reserved concurrency to 100, that function can never exceed 100 concurrent executions, but it also guarantees those 100 are always available. Functions without reserved concurrency share the unreserved pool.
Lambda's scaling model has three modes that operate on different timescales. Understanding these is critical for capacity planning.
Burst Scaling: Lambda can instantly scale from 0 to a burst limit of 500 to 3,000 concurrent executions (varies by region, with us-east-1 getting 3,000). This happens within seconds. All these environments cold-start simultaneously.
Linear Scaling: After the burst limit, Lambda adds 500 additional concurrent executions per minute. If you need 10,000 concurrent executions starting from zero, the burst gives you 3,000 instantly, then you gain 500 per minute, reaching 10,000 in 14 minutes.
Provisioned Concurrency: You pre-allocate a specific number of execution environments that stay warm at all times. These environments are fully initialized (your init code has run) and ready to serve requests with zero cold start. You pay for them even when idle.
For event-driven architectures, Lambda does not just wait for invocations. It actively polls event sources like SQS, Kinesis, and DynamoDB Streams. The event source mapping (ESM) is a Lambda-managed poller that runs inside the Lambda service itself.
SQS Polling: Lambda maintains a fleet of pollers that long-poll your SQS queue. As messages accumulate, Lambda increases the number of concurrent pollers (up to 60 initial batches, then scaling by 60 per minute). Each successful poll triggers a synchronous Lambda invocation with a batch of messages. If the function returns successfully, the messages are deleted from the queue. If it fails, messages become visible again after the visibility timeout.
Kinesis/DynamoDB Streams Polling: Lambda creates one poller per shard. Each poller reads records sequentially, maintaining a checkpoint (iterator position). This means parallelism for Kinesis equals the number of shards. With 4 shards, you get at most 4 concurrent Lambda invocations (unless you enable parallelization factor, which allows up to 10 concurrent batches per shard).
Lambda's failure behavior depends on the invocation type and the nature of the failure. This is an area where I see the most confusion among engineers, so I will be thorough.
When a synchronous invocation fails (function error, timeout, or throttle), the caller gets the error immediately. Lambda does not retry synchronous invocations. The caller is responsible for implementing retry logic.
For API Gateway integrations, a Lambda error becomes a 5xx response to the end user unless you configure custom error mapping. API Gateway also has its own 29-second timeout that is separate from (and often shorter than) your Lambda timeout. If Lambda takes 30 seconds but API Gateway times out at 29, the user gets a 504 even though Lambda might return successfully.
Asynchronous invocations have a built-in retry mechanism. When your function fails:
Lambda retries after 1 minute
If the retry fails, Lambda retries again after 2 minutes
If the third attempt fails, the event goes to the dead-letter queue (if configured) or the failure destination (if configured) or is discarded
The event stays in Lambda's internal queue for up to 6 hours. If all retries are exhausted within that window, the event is processed through the failure pipeline. If the queue retention period expires before all retries complete, the event is dropped.
For Kinesis and DynamoDB Streams, a failing record blocks the entire shard. Lambda retries the failed batch indefinitely by default, which means all records behind the failing one queue up. I have seen this backlog grow to millions of records within hours, and when the poison pill is finally resolved (by fixing the function code or increasing the timeout), the consumer processes a massive backlog of stale records.
The fix: always configure maxRetryAttempts, bisectBatchOnFunctionError, and a failure destination for stream-based event source mappings.
Failure
What Happens
How to Detect
How to Fix
Function throws unhandled exception
Synchronous: caller gets error response. Async: retried twice, then sent to DLQ/failure destination.
Cold start fails, invocation retried. If persistent, function becomes non-invocable.
Deployment errors, all invocations failing
Redeploy function, check S3 bucket permissions
Downstream service unavailable
Function runs but fails when calling DynamoDB/S3/etc.
Application-level error metrics
Implement circuit breaker pattern, retry with backoff
Memory exhaustion
Function killed with "Runtime exited with error: signal: killed"
Max Memory Used equals Memory Size in logs
Increase memory allocation or optimize data processing
Init timeout (10 seconds)
Environment creation fails, invocation retried on fresh environment
Init Duration exceeding limits
Move heavy init to lazy loading, use SnapStart for Java
The 15-minute timeout is a hard wall
Lambda functions cannot run longer than 15 minutes. There is no way to extend this. For long-running work, break the task into chunks using Step Functions, or use ECS/Fargate. I have seen teams try to work around this with recursive Lambda invocations (one Lambda calling another to continue processing), which works but is fragile and hard to debug.
At 1,769 MB, you get exactly 1 vCPU. At 10,240 MB, you get 6 vCPUs. But Lambda functions are single-threaded by default. To use multiple vCPUs, your code must explicitly use threads or child processes. Allocating 10 GB of memory for a single-threaded function wastes 5 vCPUs worth of compute that you still pay for.
I reach for Lambda when I need event-driven, short-duration processing with unpredictable traffic patterns. The auto-scaling from zero and per-millisecond billing make it unbeatable for bursty workloads. I switch to ECS Fargate when requests need more than 15 minutes, when I need container-level control, or when the concurrency model (one request per instance) becomes too expensive at high throughput. For steady, high-throughput workloads, EC2 reserved instances are still the cheapest option.
Here is the cost comparison that catches teams off guard. Lambda charges per-request and per-GB-second. At low utilization (bursty traffic), Lambda wins decisively. But at steady high throughput, the one-request-per-environment model is expensive.
Consider a service handling 1,000 concurrent requests, each taking 100ms:
Lambda (1 GB memory): 1,000 environments running constantly = 1,000 GB-seconds per second. At $0.0000166667/GB-second, that is $1.44/day of compute, plus invocation charges.
ECS Fargate (1 vCPU, 2 GB): 5 containers handling 200 concurrent requests each = $4.67/day but handling the same load with 5 containers instead of 1,000 environments.
EC2 Reserved Instance (m6g.large): $1.50/day but requires capacity management.
At steady high throughput, Lambda can be 3-10x more expensive than containers or EC2. The crossover point is typically around 30-40% utilization. Below that, Lambda's scale-to-zero advantage wins. Above that, containers are more cost-effective.
Lambda supports ARM64 (Graviton2) processors, which offer 20% better price-performance compared to x86. Switching is often as simple as changing the architecture setting in your function configuration. Most runtimes (Node.js, Python, Java, Go) run on ARM64 without code changes. The main compatibility issue is native binary dependencies (compiled C extensions for Python, native Java libraries) that must be recompiled for ARM64.
When asked "what is Lambda": "Lambda is a serverless compute service that runs code inside Firecracker microVMs. Each invocation gets a dedicated VM with hardware-level isolation. Functions scale from zero to thousands of concurrent executions automatically."
When asked about cold starts: "A cold start has five phases: placement decision (1-2ms), code download (50-200ms), microVM boot (125ms), runtime init (5ms for Node.js, 500ms+ for Java), and init code execution (varies). The biggest improvements come from reducing package size and using provisioned concurrency."
When asked about isolation: "Lambda uses Firecracker microVMs, not containers. Each function gets its own Linux kernel, so a kernel exploit in one function cannot affect another. This is the same isolation level as EC2."
When asked about scaling: "Lambda does burst-then-linear scaling. Up to 3,000 concurrent environments instantly (region-dependent), then 500 per minute after that. Provisioned concurrency eliminates cold starts by keeping environments pre-warmed."
When asked about concurrency: "Each Lambda execution environment handles exactly one request at a time. Ten concurrent requests require ten environments. This is fundamentally different from a web server that handles concurrent requests in one process."
When asked about event sources: "Lambda has two invocation models. Pull-based: Lambda polls SQS/Kinesis/DynamoDB Streams using internal pollers. Push-based: API Gateway, S3, SNS push events to Lambda. The polling model uses event source mappings that manage batching, checkpointing, and error handling."
When asked about cost optimization: "Right-size memory using Lambda Power Tuning. Memory and CPU are coupled, so the cheapest config is not always the lowest memory. Use Graviton2 (arm64) for 20% cost reduction. Use provisioned concurrency for latency-sensitive paths and SnapStart for Java to reduce cold starts from 2s to 200ms."
When asked about VPC networking: "Lambda functions in a VPC use Hyperplane ENIs (Elastic Network Interfaces) shared across execution environments. Since 2019, VPC attachment no longer adds cold start latency. The ENI is pre-created when you deploy the function, not on each invocation. Functions without VPC access use Lambda's managed networking with NAT."
When asked about limits: "15-minute max execution, 10 GB max memory, 6 MB sync payload, 250 MB unzipped deployment package, 1,000 default concurrent executions per account per region. These are hard limits except concurrency, which can be increased via support."
When asked about container image support: "Lambda supports container images up to 10 GB as a deployment format. But the image still runs inside a Firecracker microVM, not Docker. Lambda uses lazy loading to pull only the image layers needed for startup, which keeps cold starts comparable to zip deployments. Container images give you full control over the runtime, system libraries, and build process."
Lambda runs your code inside Firecracker microVMs, providing hardware-level isolation (not container isolation) with sub-125ms boot times.
Cold starts have five distinct phases: placement, code download, VM boot, runtime init, and your init code. Runtime init dominates for Java/.NET.
Warm execution environments persist for 5-15 minutes and reuse VM, runtime, global state, and /tmp storage, reducing routing overhead to under 1ms.
Each execution environment handles exactly one concurrent request. Ten simultaneous requests need ten environments.
Scaling follows a burst-then-linear model: 500-3,000 environments instantly, then 500 per minute. Provisioned concurrency eliminates cold starts.
Event source mappings are internal pollers that Lambda manages for SQS, Kinesis, and DynamoDB Streams, handling batching, checkpointing, and error retries.
The Worker Manager maintains a real-time map of every execution environment and makes sub-millisecond placement decisions for every invocation.
Memory and CPU are coupled: 1,769 MB equals 1 vCPU. Right-sizing memory is the single most impactful performance and cost optimization.
How Firecracker VMs work: The virtual machine monitor that powers Lambda's isolation model, using KVM and a minimal device model for sub-125ms boot times.
AWS Step Functions: The orchestration layer for workflows that exceed Lambda's 15-minute limit or require complex branching and error handling.
Container orchestration (ECS/Kubernetes): The alternative compute model where you manage long-running containers instead of per-invocation functions.
Event-driven architecture: The broader pattern where Lambda serves as the compute layer, reacting to events from queues, streams, and notifications.
Serverless cost optimization: Strategies including memory right-sizing, Graviton2 adoption, provisioned concurrency scheduling, and architecture-level decisions.
title: "How AWS Lambda executes your function"
description: "How Lambda manages cold starts, micro-VMs with Firecracker, execution environments, concurrency limits, and the invoke lifecycle."
tags: