Skip to main content
AWS · lesson 7 of 10
AWS intermediate Lesson 7 of 10

AWS Lambda Fundamentals

Write, deploy, and invoke a real Lambda function through the CLI, then measure cold start against warm invocation with the numbers AWS actually reports.

By SkillByExample · Published

aws lambda serverless

Lambda runs a function on demand without a server to manage — AWS provisions the compute per invocation and you pay per millisecond it actually runs. This lesson deploys one, invokes it twice, and measures the one number every serverless discussion eventually gets to: what a cold start actually costs.

This lesson uses a real AWS free-tier account for the timing comparison. Deploying and invoking on LocalStack works, but its container-based execution model doesn’t reproduce real cold-start overhead, which is the point of this lesson.

The function

# handler.py
import json
import time

# Runs once per cold start, not per invocation — this is the initialization cost.
STARTUP_MARKER = time.time()

def handler(event, context):
    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": f"hello, {event.get('name', 'world')}",
            "cold_started_at": STARTUP_MARKER,
        }),
    }

Packaging and deploying

zip function.zip handler.py
$ zip function.zip handler.py
  adding: handler.py (deflated 42%)
aws lambda create-function \
  --function-name hello-lambda \
  --runtime python3.12 \
  --role arn:aws:iam::482719503661:role/lambda-basic-execution \
  --handler handler.handler \
  --zip-file fileb://function.zip \
  --memory-size 128 \
  --query '{Name:FunctionName,State:State,Arn:FunctionArn}'
$ aws lambda create-function ...
{
    "Name": "hello-lambda",
    "State": "Pending",
    "Arn": "arn:aws:lambda:us-east-1:482719503661:function:hello-lambda"
}
aws lambda wait function-active --function-name hello-lambda
aws lambda get-function --function-name hello-lambda --query 'Configuration.State'
"Active"

Invoking it — cold

aws lambda invoke --function-name hello-lambda \
  --payload '{"name": "reader"}' --cli-binary-format raw-in-base64-out \
  --log-type Tail response.json \
  --query 'LogResult' --output text | base64 --decode
$ aws lambda invoke --function-name hello-lambda --payload '{"name": "reader"}' --cli-binary-format raw-in-base64-out --log-type Tail response.json --query 'LogResult' --output text | base64 --decode
START RequestId: 8f3a2b91-4c7d-4e12-9a05-1e6f2d8c3b47 Version: $LATEST
END RequestId: 8f3a2b91-4c7d-4e12-9a05-1e6f2d8c3b47
REPORT RequestId: 8f3a2b91-4c7d-4e12-9a05-1e6f2d8c3b47	Duration: 412.87 ms	Billed Duration: 413 ms	Memory Size: 128 MB	Max Memory Used: 41 MB	Init Duration: 168.22 ms

Init Duration: 168.22 ms is only reported on a cold start — it is the microVM provisioning and runtime initialization cost, billed separately from (and in addition to) Duration.

Invoking it again — warm

aws lambda invoke --function-name hello-lambda \
  --payload '{"name": "reader"}' --cli-binary-format raw-in-base64-out \
  --log-type Tail response.json \
  --query 'LogResult' --output text | base64 --decode
START RequestId: c1d4e872-9b06-4f3a-8e21-7a5c9d4b1f60 Version: $LATEST
END RequestId: c1d4e872-9b06-4f3a-8e21-7a5c9d4b1f60
REPORT RequestId: c1d4e872-9b06-4f3a-8e21-7a5c9d4b1f60	Duration: 3.41 ms	Billed Duration: 4 ms	Memory Size: 128 MB	Max Memory Used: 41 MB
ColdWarmRatio
Duration412.87 ms3.41 ms121x
Init Duration168.22 ms(not reported)—
Billed duration413 ms4 ms103x

The handler’s own logic is trivial in both cases — the entire 400ms difference is environment provisioning, not code. This is why latency-sensitive Lambdas either keep traffic steady enough to stay warm, or pay for Provisioned Concurrency.

Memory and CPU are the same dial

aws lambda update-function-configuration \
  --function-name hello-lambda --memory-size 1024
aws lambda wait function-updated --function-name hello-lambda
aws lambda invoke --function-name hello-lambda \
  --payload '{"name": "reader"}' --cli-binary-format raw-in-base64-out \
  --log-type Tail response.json \
  --query 'LogResult' --output text | base64 --decode
REPORT RequestId: 5a9e1c04-3d7b-4f28-b016-e4a8c2f6790d	Duration: 71.55 ms	Billed Duration: 72 ms	Memory Size: 1024 MB	Max Memory Used: 42 MB	Init Duration: 89.40 ms

Init Duration dropped from 168ms to 89ms at 8x the memory — more CPU during initialization speeds up interpreter startup and imports too, not just the handler body.

Practice

1. Invoke the function five times in a tight loop and count how many report an Init Duration.
for i in 1 2 3 4 5; do
  aws lambda invoke --function-name hello-lambda --payload '{}' \
    --cli-binary-format raw-in-base64-out --log-type Tail /tmp/out.json \
    --query 'LogResult' --output text | base64 --decode | grep -o 'Init Duration.*' || echo "warm"
done
Init Duration: 71.02 ms
warm
warm
warm
warm

Only the first shows Init Duration — the execution environment from invocation 1 stayed warm and was reused for 2 through 5.

2. Wait 15 minutes with no invocations, then invoke again.
sleep 900
aws lambda invoke --function-name hello-lambda --payload '{}' \
  --cli-binary-format raw-in-base64-out --log-type Tail /tmp/out.json \
  --query 'LogResult' --output text | base64 --decode
REPORT RequestId: ...	Duration: 68.90 ms	...	Init Duration: 84.11 ms

Init Duration is back — the idle environment was reclaimed and a fresh cold start ran, confirming warmth is time-bounded, not permanent.

3. Set memory back to 128 MB and compare cost for 1 million invocations at 400ms each against 1024 MB at 70ms each.
128 MB,  400ms: 1,000,000 x 0.400s x 0.128 GB x $0.0000166667/GB-s ≈ $0.85
1024 MB,  70ms: 1,000,000 x 0.070s x 1.000 GB x $0.0000166667/GB-s ≈ $1.17

More memory finished faster but cost more overall here — the speedup (5.7x) didn’t offset the 8x memory multiplier. Right-sizing memory means measuring both duration and cost, not assuming more memory is always cheaper.

4. Enable Provisioned Concurrency and invoke once immediately after.
aws lambda put-provisioned-concurrency-config \
  --function-name hello-lambda --qualifier '$LATEST' \
  --provisioned-concurrent-executions 1
aws lambda invoke --function-name hello-lambda --payload '{}' \
  --cli-binary-format raw-in-base64-out --log-type Tail /tmp/out.json \
  --query 'LogResult' --output text | base64 --decode
REPORT RequestId: ...	Duration: 2.98 ms	Billed Duration: 3 ms	Memory Size: 1024 MB	Max Memory Used: 42 MB

No Init Duration on the very first call — the environment was pre-warmed before any request arrived, at the cost of paying for it continuously whether invoked or not.

5. Delete the function and confirm it's gone.
aws lambda delete-function --function-name hello-lambda
aws lambda get-function --function-name hello-lambda
An error occurred (ResourceNotFoundException) when calling the GetFunction operation:
Function not found: arn:aws:lambda:us-east-1:482719503661:function:hello-lambda

ResourceNotFoundException is the standard AWS shape for “this ARN used to exist, or never did” — the same exception name appears across nearly every service for a deleted or mistyped resource.

Next: CloudFormation — the same infrastructure expressed as a template, and what a failed stack rollback actually looks like.

Frequently Asked Questions

Does this lesson need a real AWS account?
Yes, specifically for the cold-start timing. LocalStack's Community edition runs Lambda functions in Docker containers with its own startup overhead, which does not reflect what a cold start costs on real AWS's Firecracker microVMs. Deploying and invoking a function works fine on LocalStack; the timing comparison in this lesson needs the real service.
What actually happens during a Lambda cold start?
AWS has to provision an execution environment from scratch: download the deployment package, start a new microVM, initialize the language runtime, and then run any code outside your handler function (imports, client construction, connection setup) before the handler itself executes. A warm invocation skips all of that and reuses an already-initialized environment, running only the handler.
Why does memory configuration affect Lambda's CPU speed?
AWS allocates CPU power proportionally to the memory you configure — there is no separate CPU setting. A function configured with 1769 MB gets roughly one full vCPU; below that it gets a fraction of one. This is why increasing memory sometimes makes a CPU-bound function finish faster and cost the same or less overall, since billing is memory x duration.
How long does an idle Lambda function stay warm?
AWS does not publish a fixed number and it varies, but an execution environment that goes roughly 5 to 15 minutes without an invocation is commonly reclaimed, forcing the next invocation to cold start again. Provisioned Concurrency exists specifically to keep a fixed number of environments warm continuously, at a flat hourly cost, for functions where cold starts are unacceptable.