Serverless with Lambda

AWS Lambda runs your code in response to events with no servers to manage, automatic scaling, and billing per millisecond of execution time.

Introduction

Lambda is the purest form of serverless compute: you write a function, define what should trigger it, and AWS handles every server, container, runtime update, and scaling event. You pay only for the compute time your code actually uses, measured in GB-seconds. A function that runs for 100ms at 256MB costs about $0.00000043. Zero cost when idle.

1. Execution Model and Cold Starts

Lambda Execution Lifecycle

EVBTriggerλCold StartInit code runsλWarm InvocationHandler onlyλhandler()first callreuse~100-500ms~1ms

Cold starts happen when Lambda spins up a new execution environment; subsequent calls reuse it (warm)

Cold Start

When no warm execution environment exists, Lambda downloads your code, starts a container, and runs your initialization code (imports, SDK clients, DB connections). This adds 100ms-500ms for Python functions. Minimize imports and use connection pooling outside the handler.

Warm Invocation

If a previous execution environment is available, Lambda reuses it. The handler function runs directly without initialization overhead. Global variables and SDK clients persist between warm invocations, initialize them outside the handler.

2. The Handler Function

Every Lambda function has a single entry point: a function that receives an event and context argument. The event shape depends on the trigger.

import json
import boto3

def handler(event, context):
    """
    event   - the trigger payload (dict)
    context - runtime metadata (request ID, remaining time, etc.)
    """
    print(f"Request ID: {context.aws_request_id}")
    print(f"Remaining time: {context.get_remaining_time_in_millis()}ms")

    # Return a response (for API Gateway triggers)
    return {
        "statusCode": 200,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps({"message": "Hello from Lambda"}),
    }

Initialize SDK clients outside the handler to benefit from warm reuse:

import boto3

# Module-level: runs once per container, reused on warm invocations
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("Orders")

def handler(event, context):
    return table.get_item(Key={"orderId": event["orderId"]})

3. Triggers

Lambda can be triggered by dozens of AWS services. The most common patterns:

API Gateway

HTTP requests from the web. The event contains headers, query params, and body.

S3

Object created/deleted events. Process uploads as they arrive.

SQS

Queue messages delivered in batches. Lambda polls the queue automatically.

EventBridge

Schedule (cron) or rule-based events. Great for async workflows and scheduled tasks.

DynamoDB Streams

Item-level change events from a DynamoDB table. Use for event sourcing.

Direct Invoke

Called synchronously or asynchronously from another Lambda, SDK, or CLI.

S3 trigger example - process every uploaded file:

import boto3

s3 = boto3.client("s3")

def handler(event, context):
    # S3 can batch multiple records in one invocation
    for record in event["Records"]:
        bucket = record["s3"]["bucket"]["name"]
        key = record["s3"]["object"]["key"]
        size = record["s3"]["object"]["size"]

        print(f"New file: s3://{bucket}/{key} ({size} bytes)")

        # Process the file - e.g., generate a thumbnail
        response = s3.get_object(Bucket=bucket, Key=key)
        content = response["Body"].read()
        # ... processing logic here

SQS trigger example - reliable async job processing:

import json

def handler(event, context):
    for record in event["Records"]:
        # SQS message body is a string, parse if JSON
        body = json.loads(record["body"])
        receipt_handle = record["receiptHandle"]

        print(f"Processing order: {body.get('orderId')}")

        try:
            process_order(body)
            # SQS auto-deletes the message on successful return
        except Exception as e:
            # Raising an exception causes SQS to retry
            # After maxReceiveCount retries, message goes to DLQ
            raise e

def process_order(order: dict):
    # Your business logic here
    pass

4. Configuration, Layers, and Limits

Memory and CPU

Configure 128MB to 10,240MB. CPU is allocated proportionally to memory, doubling memory doubles vCPU. For CPU-bound work, increase memory even if you don't need it.

Layers

Zip archives with libraries, data, or configuration shared across functions. Add up to 5 layers per function. Use for common dependencies (pandas, PIL) to keep deployment packages small.

Key Limits
  • Max timeout: 15 minutes
  • Deployment package: 50MB (zipped), 250MB (unzipped)
  • /tmp storage: 512MB to 10GB
# Package your function
zip function.zip lambda_function.py

# Create the Lambda function
aws lambda create-function \
  --function-name my-function \
  --runtime python3.12 \
  --role arn:aws:iam::123456789012:role/MyLambdaRole \
  --handler lambda_function.handler \
  --zip-file fileb://function.zip \
  --timeout 30 \
  --memory-size 256 \
  --environment 'Variables={TABLE_NAME=Orders,STAGE=prod}'

# Update the code only (faster than recreating)
aws lambda update-function-code \
  --function-name my-function \
  --zip-file fileb://function.zip

# Invoke synchronously and see the response
aws lambda invoke \
  --function-name my-function \
  --payload '{"test": true}' \
  --cli-binary-format raw-in-base64-out \
  response.json && cat response.json

5. Durable Functions

Durable Functions let you build resilient multi-step workflows that run for up to one year inside Lambda, without managing state yourself. The SDK uses a checkpoint/replay model: every context.step() call persists its result. On failure or interruption, the function re-executes from the start but replays through completed steps instantly, resuming only from where it left off. context.wait() and context.wait_for_callback() suspend the function entirely, incurring zero compute charges during the pause.

context.step()

Wraps a unit of work with automatic checkpointing and retries on transient failures. The result is stored so replay skips the call entirely.

context.wait()

Suspends execution for a fixed duration. The function stops running and Lambda recycles the environment. You pay nothing during the wait.

wait_for_callback()

Suspends until an external system posts back a result (human approval, webhook, etc.). Can wait hours or days at zero cost.

Example: distributed transaction across microservices

If payment fails on the third call, the function replays, skips the already-checkpointed inventory reservation, and retries only from the payment step.

from aws_durable_execution_sdk_python import DurableContext, durable_execution

@durable_execution
def lambda_handler(event, context: DurableContext):
    order_id = event['orderId']
    amount = event['amount']
    items = event['items']

    # Each step is checkpointed: on replay, completed steps are skipped
    inventory = context.step(
        lambda _: inventory_service.reserve(items),
        name='reserve-inventory'
    )

    payment = context.step(
        lambda _: payment_service.charge(amount),
        name='process-payment'
    )

    shipment = context.step(
        lambda _: shipping_service.create_shipment(order_id, inventory),
        name='create-shipment'
    )

    return {'orderId': order_id, 'status': 'completed', 'shipment': shipment}

Example: human-in-the-loop approval

The function suspends at wait_for_callback for up to 24 hours with zero compute cost, then resumes automatically when the reviewer posts back.

from aws_durable_execution_sdk_python import DurableContext, durable_execution, WaitConfig

@durable_execution
def lambda_handler(event, context: DurableContext):
    doc_id = event['documentId']
    reviewers = event['reviewers']

    context.step(
        lambda _: document_service.prepare(doc_id),
        name='prepare-document'
    )

    # Suspend here - zero compute charges while waiting up to 24 hours
    approval = context.wait_for_callback(
        lambda callback_id: notification_service.send_approval_request({
            'documentId': doc_id,
            'reviewers':  reviewers,
            'callbackId': callback_id,
            'expiresIn':  86400,
        }),
        name='approval-callback',
        config=WaitConfig(timeout=86400),
    )

    if approval and approval.get('approved'):
        context.step(
            lambda _: document_service.finalize(doc_id, approval.get('comments')),
            name='finalize-document'
        )
        return {'status': 'approved', 'documentId': doc_id}

    context.step(
        lambda _: document_service.archive(doc_id, approval.get('reason')),
        name='archive-rejected'
    )
    return {'status': 'rejected', 'documentId': doc_id}
Other primitives
  • context.parallel() - run steps concurrently, wait for all to finish
  • context.map() - process an array with controlled concurrency
  • context.invoke() - call another Lambda function and checkpoint the result
Durable Functions vs Step Functions

Use Durable Functions when your workflow is tightly coupled with application code in Lambda and you prefer standard Python over a graph DSL. Use Step Functions when you need native integrations with 220+ AWS services, visual design tools, or zero-maintenance orchestration across multiple services.

6. Code Signing with AWS Signer

Without code signing, anyone with sufficient IAM permissions can deploy arbitrary code to a Lambda function, including a compromised CI/CD pipeline or a malicious insider. AWS Signer lets you cryptographically sign deployment packages so that Lambda verifies the signature before every deploy, blocking unsigned or tampered artifacts at the gate. This is a key control for SOC2, PCI-DSS, and other compliance frameworks that require supply chain integrity.

1
Create Signing Profile

A signing profile is a named identity tied to a specific platform. For Lambda, use AWSLambda-SHA384-ECDSA. The profile version ARN is used when building the code signing configuration.

2
Sign the Package

Upload your zip to S3 (versioning required) and run a signing job. AWS Signer produces a new signed zip in the destination prefix. Check the job status before deploying.

3
Create Signing Config

A code signing configuration holds the list of trusted signing profile ARNs and the enforcement policy. One config can be shared across many functions.

4
Attach to Function

Link the code signing config to the Lambda function. From that point on, every deploy is verified against the config - use the signed S3 artifact, not the original zip.

WARN mode

Unsigned or unrecognized artifacts trigger a CloudWatch log warning, but the deployment still proceeds. Useful for a gradual rollout: start in WARN to surface violations before switching to Enforce.

ENFORCE mode

Deployments from unsigned, expired, or revoked packages are blocked outright with a CodeSigningConfigMismatch error. Use this in production once all pipelines are signing correctly.

# 1. Create a signing profile (Lambda-specific ECDSA platform)
aws signer put-signing-profile \
  --profile-name MyLambdaProfile \
  --platform-id AWSLambda-SHA384-ECDSA

# 2. Sign the deployment package (bucket must have versioning enabled)
aws signer start-signing-job \
  --source 's3={bucketName=my-deploy-bucket,key=function.zip,version=S3_VERSION_ID}' \
  --destination 's3={bucketName=my-deploy-bucket,prefix=signed/}' \
  --profile-name MyLambdaProfile

# Check signing job status
aws signer describe-signing-job --job-id JOB_ID

# 3. Create a code signing configuration (Enforce blocks unsigned deploys)
aws lambda create-code-signing-config \
  --description "Production signing policy" \
  --allowed-publishers SigningProfileVersionArns=arn:aws:signer:us-east-1:123456789012:/signing-profiles/MyLambdaProfile/versions/VERSION_ARN \
  --code-signing-policies UntrustedArtifactOnDeployment=Enforce

# 4. Attach the config to your function
aws lambda put-function-code-signing-config \
  --function-name my-function \
  --code-signing-config-arn arn:aws:lambda:us-east-1:123456789012:code-signing-config:csc-0123456789abcdef0

# 5. Deploy using the signed artifact from S3
aws lambda update-function-code \
  --function-name my-function \
  --s3-bucket my-deploy-bucket \
  --s3-key signed/function.zip
S3 versioning required

The start-signing-job source requires an S3 object version ID, so the deploy bucket must have versioning enabled. Enable it once with aws s3api put-bucket-versioning --bucket my-deploy-bucket --versioning-configuration Status=Enabled.

Key Takeaways

  • Cold starts - minimize them by keeping package sizes small and initializing SDK clients at module level (not inside the handler)
  • Handler signature - always handler(event, context); the event shape depends entirely on the trigger source
  • Triggers - API GW for HTTP, S3 for file events, SQS for queue processing, EventBridge for scheduled or rule-based invocations
  • SQS integration - Lambda polls automatically; raise an exception to force a retry; messages go to DLQ after max retries
  • Layers - share common dependencies (numpy, boto3 extensions) across functions without bloating each deployment package
  • Cost model - charged per GB-second; increase memory for CPU-bound tasks since it also increases vCPU allocation
  • Durable Functions - checkpoint/replay SDK for multi-step workflows up to 1 year; step() retries automatically, wait() suspends at zero cost, wait_for_callback() handles human-in-the-loop patterns
  • Code Signing - attach a code signing configuration so only AWS Signer-approved packages can be deployed; Enforce mode blocks unsigned or tampered artifacts at deploy time, making it a key supply chain control