Lambda Execution Model: Handlers, Context, Cold Starts, and the Execution Environment
A Lambda function is a handler function inside an execution environment. The environment is reused across invocations (warm starts) but not guaranteed to exist (cold starts). Understanding the execution environment lifecycle — init, invoke, shutdown — explains why you initialize clients outside the handler and why cold start latency varies by runtime and memory.
The handler: function entry point
The Lambda handler is the function AWS calls when your function is invoked. You specify it in the function configuration as filename.function_name.
# handler = "lambda_function.handler"
import json
import boto3
# This code runs ONCE during environment initialization (outside handler)
s3_client = boto3.client('s3')
ssm_client = boto3.client('ssm')
# Pre-load configuration on cold start
config = ssm_client.get_parameter(Name='/myapp/config', WithDecryption=True)
def handler(event, context):
# This code runs on EVERY invocation
bucket = event.get('bucket')
key = event.get('key')
response = s3_client.get_object(Bucket=bucket, Key=key)
return {
'statusCode': 200,
'body': json.dumps({'size': response['ContentLength']})
}
The handler receives two arguments:
event: the triggering event payload. Shape depends on the trigger source (API Gateway, S3, SQS, EventBridge all send different formats).context: runtime information about the current invocation.
The execution environment lifecycle
ConceptAWS LambdaLambda execution environments go through three phases: Init (download code, start runtime, run initialization code outside handler), Invoke (call the handler), and Shutdown (environment is frozen or terminated). The same environment can be reused for multiple invocations — this is a warm start. A new environment must be created when no warm environment is available — this is a cold start.
Prerequisites
- function invocation basics
- container runtimes
- connection pooling
Key Points
- Code outside the handler runs once during Init phase — not on every invocation.
- SDK clients, database connections, and loaded configuration persist between invocations in the same environment.
- Cold start = Init phase duration. Ranges from ~100ms (Python, Node.js) to ~1-2s (Java, .NET).
- Execution environments are single-threaded per invocation — no concurrency within one environment.
The context object: invocation metadata
The context object provides information about the current invocation:
def handler(event, context):
# Time remaining before Lambda times out
remaining_ms = context.get_remaining_time_in_millis()
# Unique invocation ID (useful for logging and idempotency)
invocation_id = context.aws_request_id
# Function name and version
function_name = context.function_name
function_version = context.function_version # "$LATEST" or version number
# Memory limit configured for this function
memory_limit = context.memory_limit_in_mb
# Log stream name for this invocation
log_stream = context.log_stream_name
# Use remaining time to decide whether to start expensive operations
if remaining_ms < 5000: # less than 5 seconds left
return {'statusCode': 408, 'body': 'Not enough time to process'}
get_remaining_time_in_millis() is particularly useful for long-running functions that might time out mid-operation. Check remaining time before starting multi-step processes to avoid partial execution.
Cold starts: what they are and how to reduce them
A cold start happens when Lambda needs to create a new execution environment. Duration:
- Infrastructure allocation: Lambda finds capacity and allocates a microVM
- Environment setup: download function code/container, extract, prepare runtime
- Init phase: run code outside the handler (import libraries, initialize clients)
Cold start duration varies significantly by runtime and function size:
| Runtime | Typical cold start | Memory impact | |---|---|---| | Node.js 20.x | 100–300ms | Higher memory = slightly faster | | Python 3.12 | 100–400ms | Minimal | | Java 21 | 1–3s | Higher memory significantly faster | | .NET 8 | 500ms–2s | Moderate | | Container image | 2–10s | Depends on image size |
Reducing cold start impact:
# 1. Initialize expensive objects once, outside handler
import boto3
from sqlalchemy import create_engine
# These run once per cold start, cached for warm invocations
db_engine = create_engine(
os.environ['DATABASE_URL'],
pool_size=1, # Lambda is single-threaded, 1 connection per env
max_overflow=0
)
s3 = boto3.client('s3')
def handler(event, context):
# Reuses the already-initialized engine and s3 client
with db_engine.connect() as conn:
result = conn.execute(text("SELECT 1")).fetchone()
# 2. Provisioned Concurrency: pre-warm N environments
resource "aws_lambda_provisioned_concurrency_config" "api" {
function_name = aws_lambda_function.api.function_name
qualifier = aws_lambda_alias.api_live.name
provisioned_concurrent_executions = 5 # 5 pre-warmed environments
}
Provisioned Concurrency keeps environments warm and eliminates cold start latency. Cost: you pay for the configured concurrency whether it's used or not.
📝Lambda invocation types and error handling
Lambda has three invocation types with different error semantics:
Synchronous (RequestResponse): caller waits for function to complete and return. API Gateway integration is synchronous. Lambda returns the function's return value, or propagates errors to the caller.
Asynchronous (Event): caller receives acknowledgment immediately; Lambda queues the invocation. S3 event notifications, SNS triggers, and EventBridge rules are asynchronous. Lambda retries twice on failure (configurable). Configure a Dead Letter Queue (DLQ) or destination for failed invocations:
resource "aws_lambda_function" "processor" {
# ...
dead_letter_config {
target_arn = aws_sqs_queue.dlq.arn
}
}
Poll-based (event source mappings): Lambda polls SQS queues, Kinesis streams, and DynamoDB streams. Failures within a batch can be handled with partial batch reporting (return the failed item IDs; Lambda retries only those items):
def handler(event, context):
batch_item_failures = []
for record in event['Records']:
try:
process(record)
except Exception as e:
# Report this specific record as failed
batch_item_failures.append({
"itemIdentifier": record['messageId']
})
return {"batchItemFailures": batch_item_failures}
Without partial batch failure reporting, any single record failure causes Lambda to retry the entire batch — including records that already succeeded.
Function URLs vs API Gateway
Lambda Function URLs provide a direct HTTPS endpoint without API Gateway:
resource "aws_lambda_function_url" "api" {
function_name = aws_lambda_function.api.function_name
authorization_type = "AWS_IAM" # or "NONE" for public
cors {
allow_credentials = true
allow_origins = ["https://app.example.com"]
allow_methods = ["GET", "POST"]
allow_headers = ["*"]
max_age = 86400
}
}
Function URLs are useful for simple endpoints where API Gateway features (request validation, API keys, usage plans, custom domain mapping) aren't needed. They support Lambda response streaming natively.
Use API Gateway when you need: multiple functions behind one domain, authentication via Cognito/custom authorizers, request/response transformation, WAF integration, or usage-based throttling.
A Lambda function initializes a database connection pool outside the handler. During load testing, you observe that the database server hits its connection limit even though only 100 concurrent Lambda executions are configured. Why?
mediumLambda function has reserved concurrency of 100. Each execution environment initializes one database connection outside the handler. The database connection limit is 100.
ALambda creates multiple threads per execution environment
Incorrect.Lambda execution environments are single-threaded. One execution environment handles one invocation at a time.BExecution environments persist after invocations finish — 100 concurrent invocations may use environments from a larger pool, each holding an open connection
Correct!Lambda execution environments are reused for warm starts, but they persist beyond individual invocations. If you have 100 concurrent invocations using 100 execution environments, and then those invocations finish, the 100 environments remain alive (warm) waiting for the next invocation. If traffic scales down but environments stay warm, you could have 100 idle connections. Additionally, Lambda may have created MORE than 100 environments historically (some may have been recycled), each having connected at init time. Use an RDS Proxy to pool connections between Lambda and the database — the proxy maintains a connection pool on the DB side while each Lambda environment talks to the proxy.CLambda ignores reserved concurrency settings under load
Incorrect.Reserved concurrency is a hard limit. Lambda will not exceed it. The problem is the number of persistent connections per environment.DThe connection pool is being shared across invocations, causing connection leaks
Incorrect.Sharing the pool across invocations is exactly the intended behavior for warm start performance. The issue isn't leaking — it's the number of environments maintaining connections simultaneously.
Hint:Reserved concurrency limits concurrent invocations, but what about the execution environments that persist after invocations finish?