AWS Lambda Execution Lifecycle Explained
By Oleksandr Andrushchenko — Published on — Modified on
AWS Lambda execution lifecycle explains what happens from the moment a Lambda function is invoked until the function finishes and the execution environment is either reused or removed. Understanding this lifecycle helps explain cold starts, warm starts, global variable reuse, SDK client reuse, database connection behavior, and performance optimization.
Many Lambda problems become easier to understand when you know the lifecycle. Why does code outside the handler sometimes run once and sometimes again? Why can global variables survive between invocations? Why should SDK clients usually be created outside the handler? Why should you not store user-specific state globally? The execution lifecycle answers all of these questions.
Table of Contents
- What Is the Lambda Execution Lifecycle?
- Step 1: Invocation Starts
- Step 2: Execution Environment Creation
- Step 3: Initialization Phase
- Step 4: Invocation Phase
- Step 5: Freeze Phase
- Step 6: Warm Start
- When Does Lambda Create a Cold Start?
- Understanding Global Variables
- Execution Lifecycle Timeline
- Complete Lifecycle Example
- Best Practices
- Common Misconceptions
- Execution Lifecycle Checklist
- Conclusion
What Is the Lambda Execution Lifecycle?
The Lambda execution lifecycle is the sequence of steps AWS Lambda follows to run your function. At a high level, Lambda receives an event, prepares an execution environment if needed, runs initialization code, calls your handler, returns the result, and may reuse the environment later.
Event arrives
-> Lambda finds or creates execution environment
-> Init phase runs if needed
-> Handler is invoked
-> Function returns or fails
-> Environment may be frozen and reused
The lifecycle matters because not every invocation starts from zero. Some invocations are cold, where Lambda creates a new environment. Others are warm, where Lambda reuses an existing environment.
| Concept | Meaning |
|---|---|
| Event | Input that causes the function to run |
| Execution environment | Runtime environment where your function code runs |
| Init phase | Runtime setup, code loading, and global initialization |
| Invoke phase | Handler execution for one event |
| Freeze phase | Environment is paused after invocation |
| Warm start | Existing environment is reused for another invocation |
Step 1: Invocation Starts
The lifecycle begins when something invokes the Lambda function. That something can be an HTTP request, an S3 event, an SQS message, an EventBridge event, a DynamoDB Stream record, or a direct call through the AWS SDK.
Synchronous Invocations
In a synchronous invocation, the caller waits for the Lambda response. This is common for APIs.
- API Gateway invokes Lambda and waits for the response.
- Lambda Function URL invokes Lambda directly over HTTPS.
- Application Load Balancer can route requests to Lambda.
- AWS SDK can call Lambda and wait for the result.
import json
def lambda_handler(event, context):
return {
"statusCode": 200,
"body": json.dumps({
"message": "Hello from synchronous Lambda"
})
}
Asynchronous Invocations
In an asynchronous invocation, the event source sends the event to Lambda and does not wait for the function result. Lambda queues the event internally and processes it later.
- S3 can invoke Lambda after an object is uploaded.
- SNS can invoke Lambda after a message is published.
- EventBridge can invoke Lambda when a matching event arrives.
Event Source Mappings
An event source mapping is used when Lambda polls records from a stream or queue and invokes your function with a batch of records.
- SQS
- Kinesis Data Streams
- DynamoDB Streams
- Amazon MSK
- Self-managed Kafka
def lambda_handler(event, context):
for record in event["Records"]:
print("Processing record:", record)
return {
"processed": len(event["Records"])
}
Key point: the invocation style affects retries, batching, scaling, and error handling.
Step 2: Execution Environment Creation
Before Lambda can run your code, it needs an execution environment. This environment contains the runtime, your function code, dependencies, memory, temporary storage, and configuration.
When Lambda Creates a New Environment
Lambda creates a new environment when no suitable warm environment is available.
- First invocation after deployment or inactivity.
- Traffic spike that requires more concurrent environments.
- New function version or configuration change.
- Previous environments were removed after being idle.
What the Environment Contains
| Environment Part | Purpose |
|---|---|
| Runtime | Python, Node.js, Java, Go, .NET, or custom runtime |
| Function code | Your deployment package or container image |
| Dependencies | Libraries required by your function |
| Memory and CPU | Resources assigned to the function |
| /tmp storage | Temporary local storage |
| Environment variables | Configuration available to the function |
Container Reuse
Developers often say Lambda “reuses containers.” More precisely, Lambda may reuse the same execution environment for multiple invocations. This reuse is useful for performance, but it is not guaranteed.
Rule of thumb: reuse execution environment features for optimization, not for correctness.
Step 3: Initialization Phase
The initialization phase, often called INIT, runs before the handler. It happens during cold starts and when provisioned concurrency prepares environments.
INIT phase:
Load runtime
-> Load function code
-> Run imports
-> Run global code
-> Initialize clients and dependencies
Loading the Runtime
Lambda starts the configured runtime, such as Python, Node.js, Java, or Go. Runtime startup time can affect cold starts.
Loading Your Code
Lambda loads your function code and dependencies. Large packages and heavy dependency trees can increase initialization time.
Running Global Initialization Code
Code outside the handler runs during initialization.
import os
import boto3
# Runs during INIT
TABLE_NAME = os.environ["TABLE_NAME"]
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(TABLE_NAME)
def lambda_handler(event, context):
# Runs during INVOKE
response = table.get_item(
Key={"id": event["id"]}
)
return response.get("Item")
Initializing Dependencies
Frameworks, SDKs, logging libraries, configuration loaders, and database clients may all do work during initialization. This can improve warm invocation performance but increase cold start time.
| Initialization Choice | Benefit | Risk |
|---|---|---|
| Create SDK client globally | Reuse during warm starts | Slightly longer cold start |
| Load huge framework globally | Convenient routing/middleware | Longer cold start |
| Call remote config service globally | Config ready before handler | Cold start depends on network |
| Lazy-load rare dependency | Faster cold start for common path | First request using dependency pays the cost |
Creating SDK Clients
Creating SDK clients outside the handler is usually a good practice. The client can be reused across warm invocations.
import boto3
s3_client = boto3.client("s3")
def lambda_handler(event, context):
response = s3_client.list_buckets()
return {
"bucketCount": len(response["Buckets"])
}
Key point: INIT work is paid during cold starts, but it can make warm invocations faster.
Step 4: Invocation Phase
The invocation phase, or INVOKE, is when Lambda calls your handler function for a specific event.
Calling the Handler
Lambda calls the configured handler and passes two main arguments: event and context.
def lambda_handler(event, context):
return {
"message": "Handler called"
}
Reading the Event
The event object contains input from the event source. Its structure depends on the trigger.
| Event Source | Event Contains |
|---|---|
| API Gateway | HTTP method, path, headers, query parameters, body |
| S3 | Bucket name, object key, event name |
| SQS | Message records and message bodies |
| DynamoDB Streams | Inserted, modified, or removed item data |
| EventBridge | Source, detail type, and event detail |
Using the Context Object
The context object contains metadata about the invocation, including the request ID, function name, memory limit, and remaining execution time.
def lambda_handler(event, context):
return {
"requestId": context.aws_request_id,
"functionName": context.function_name,
"remainingMs": context.get_remaining_time_in_millis()
}
Executing Business Logic
During invocation, your function may validate input, call AWS services, query databases, publish events, process files, or return an API response.
def lambda_handler(event, context):
order_id = event["orderId"]
validate_order(order_id)
reserve_inventory(order_id)
publish_order_validated_event(order_id)
return {
"status": "validated",
"orderId": order_id
}
Returning the Response
The expected response depends on the event source. API Gateway expects an HTTP-like response. SQS workers often return batch failure information. EventBridge handlers may simply return a status object.
import json
def lambda_handler(event, context):
return {
"statusCode": 200,
"headers": {
"Content-Type": "application/json"
},
"body": json.dumps({
"message": "OK"
})
}
Step 5: Freeze Phase
After the handler finishes, Lambda may keep the execution environment available for reuse. During this time, the environment is effectively paused or frozen.
What Lambda May Preserve
- Global variables
- Initialized SDK clients
- Cached configuration
- Open database connections, if still valid
- Files in /tmp, while the environment exists
What Is Not Guaranteed
- The environment may be removed at any time.
- Global variables may disappear on the next invocation.
- Database connections may become stale.
- /tmp storage is not durable application storage.
- There is no guarantee that the same user or request returns to the same environment.
Rule of thumb: Lambda may preserve data for performance, but your application logic must work if nothing is preserved.
Step 6: Warm Start
A warm start happens when Lambda reuses an existing execution environment. In this case, INIT does not run again. Lambda calls the handler directly.
Warm invocation:
Existing environment
-> Handler runs
-> Response returned
Reusing Variables
Global variables can survive across warm invocations.
counter = 0
def lambda_handler(event, context):
global counter
counter += 1
return {
"counter": counter
}
This counter may increase during warm invocations, but you should never depend on it for correctness.
Reusing SDK Clients
Reusing SDK clients is a good use of warm starts.
import boto3
sqs_client = boto3.client("sqs")
def lambda_handler(event, context):
sqs_client.send_message(
QueueUrl="https://sqs.us-east-1.amazonaws.com/123/orders",
MessageBody="hello"
)
return {
"status": "sent"
}
Reusing Database Connections
Database connections can sometimes be reused, but they require validation because the connection may become stale between invocations.
import os
import psycopg2
connection = None
def get_connection():
global connection
if connection is None or connection.closed:
connection = psycopg2.connect(
host=os.environ["DB_HOST"],
dbname=os.environ["DB_NAME"],
user=os.environ["DB_USER"],
password=os.environ["DB_PASSWORD"]
)
return connection
def lambda_handler(event, context):
conn = get_connection()
with conn.cursor() as cursor:
cursor.execute("SELECT now()")
row = cursor.fetchone()
return {
"databaseTime": str(row[0])
}
Important: for production relational database workloads, consider RDS Proxy and reserved concurrency.
When Does Lambda Create a Cold Start?
A cold start happens when Lambda needs a new execution environment. This is normal behavior and not necessarily a problem.
Scaling Out
If traffic increases and existing environments are busy, Lambda creates more environments.
10 concurrent requests
-> 10 execution environments may be needed
Idle Environment Cleanup
Lambda may remove execution environments after they have been idle. A later request may need a new environment.
Deploying a New Version
When you deploy new code or change configuration, Lambda may need new environments for the updated version.
Cold Start Triggers
| Trigger | Why Cold Start Happens |
|---|---|
| First invocation | No warm environment exists yet |
| Traffic spike | More concurrent environments are needed |
| Idle period | Previous environment may have been removed |
| New deployment | New code/configuration needs new environment |
| Provisioned concurrency scale change | New provisioned environments are initialized |
Understanding Global Variables
Global variables in Lambda are often misunderstood. They can be useful for caching and client reuse, but dangerous for request-specific state.
Good Uses
- AWS SDK clients
- Database connection helpers
- Cached configuration
- Compiled regular expressions
- Reusable static data
import re
email_pattern = re.compile(r"^[^@]+@[^@]+\.[^@]+$")
def lambda_handler(event, context):
email = event["email"]
return {
"valid": bool(email_pattern.match(email))
}
Bad Uses
- Current user ID
- Request body
- Authorization state
- Temporary business workflow state
- Assumptions that the same environment will be reused
# Bad: request-specific global state
current_order_id = None
def lambda_handler(event, context):
global current_order_id
current_order_id = event["orderId"]
return process_order(current_order_id)
Caching Between Invocations
Caching between invocations can improve performance, but the cache must be treated as optional.
cached_config = None
def get_config():
global cached_config
if cached_config is None:
cached_config = load_config()
return cached_config
def lambda_handler(event, context):
config = get_config()
return {
"featureEnabled": config["featureEnabled"]
}
Rule of thumb: global cache is an optimization, not a database.
Execution Lifecycle Timeline
The lifecycle can be summarized as a timeline:
Cold invocation timeline:
1. Event arrives
2. Lambda selects function version
3. New execution environment is created
4. Runtime starts
5. Function code is loaded
6. Global initialization code runs
7. Handler is called
8. Handler returns or fails
9. Environment is frozen
10. Environment may be reused later
Warm invocation timeline:
1. Event arrives
2. Existing execution environment is selected
3. Handler is called
4. Handler returns or fails
5. Environment is frozen again
Complete Lifecycle Example
Consider this Lambda function:
import time
import boto3
print("INIT: loading module")
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("Users")
initialized_at = time.time()
def lambda_handler(event, context):
print("INVOKE: handler called")
return {
"initializedAt": initialized_at,
"requestId": context.aws_request_id
}
First Invocation
During the first cold invocation:
- Python runtime starts.
- Module-level code runs.
print("INIT: loading module")runs.dynamodbclient is created.initialized_atis set.- Handler runs and returns a response.
Second Invocation
If the environment is reused:
- Module-level code does not run again.
dynamodbclient is reused.initialized_atkeeps the previous value.- Handler runs again with a new event.
Traffic Spike Example
If ten requests arrive at the same time, Lambda may need multiple execution environments.
Request 1 -> Environment A -> INIT runs
Request 2 -> Environment B -> INIT runs
Request 3 -> Environment C -> INIT runs
Later:
Request 4 -> Environment A -> warm start
Request 5 -> Environment B -> warm start
Key point: global variables are shared only inside the same execution environment, not across all concurrent invocations.
Best Practices
Keep Initialization Small
Only put reusable and necessary setup outside the handler. Avoid expensive network calls or heavy work during initialization unless it is intentionally needed.
# Good: reusable client
import boto3
s3_client = boto3.client("s3")
def lambda_handler(event, context):
return s3_client.list_buckets()
# Risky: remote call during cold start
config = load_config_from_remote_api()
def lambda_handler(event, context):
return config
Reuse Clients Carefully
Reuse AWS SDK clients and other safe clients. For databases, validate stale connections and use pooling solutions such as RDS Proxy when needed.
Do Not Depend on Warm Starts
Warm starts are an optimization. Your function must work correctly even if every invocation is a cold start.
Write Stateless Functions
Store durable state in external systems such as S3, DynamoDB, RDS, SQS, or ElastiCache. Do not store important business state only in memory or /tmp.
Common Misconceptions
| Misconception | Reality |
|---|---|
| Lambda has no servers. | Servers exist, but AWS manages them. |
| Global variables are always reset. | They may survive warm invocations. |
| Global variables are shared across all invocations. | They are only shared within one execution environment. |
| Warm starts are guaranteed. | They are not guaranteed. |
| /tmp is permanent storage. | It is temporary and tied to the execution environment. |
| Cold starts are always the main problem. | Slow database queries, external APIs, and poor architecture are often bigger problems. |
Execution Lifecycle Checklist
- Know what runs during INIT and what runs during INVOKE.
- Keep global initialization small and predictable.
- Create reusable SDK clients outside the handler.
- Do not store request-specific state globally.
- Treat global caches as optional.
- Do not depend on warm starts for correctness.
- Use /tmp only for temporary files.
- Validate database connections before reuse.
- Use RDS Proxy for relational database workloads when needed.
- Measure Init Duration when investigating cold starts.
- Design functions as stateless units of work.
Conclusion
AWS Lambda execution lifecycle explains how Lambda really runs your code. An event arrives, Lambda finds or creates an execution environment, initialization runs if needed, the handler processes the event, and the environment may be frozen and reused later.
Understanding this lifecycle helps you write better Lambda functions. It explains why cold starts happen, why SDK clients should often be created outside the handler, why global variables may survive, why database connections need care, and why Lambda functions should remain stateless.
Key takeaway: use execution environment reuse for performance, but never depend on it for correctness. Production Lambda functions should be stateless, idempotent, observable, and designed to work correctly on both cold and warm invocations.
Comments (0)