This tutorial walks through how to enable SnapStart container image Lambda functions end to end: build the image, deploy it, turn SnapStart on, measure the difference, and fix the post-restore uniqueness issue before it bites you in production. By the end you’ll have a working function and real before/after latency numbers, not just theory.
Prerequisites
- An AWS account with permissions for Lambda, ECR, and IAM
- AWS CLI v2 installed and configured (
aws configure) - Docker installed and running locally
- Python 3.12 (the example function is Python, but the same steps apply to any language via a container image)
SnapStart Container Image Lambda Functions: Step-by-Step
-
Create the project and handler. Make a directory with a simple handler that simulates a heavier init phase — the kind of thing SnapStart is actually useful for.
mkdir snapstart-container-demo && cd snapstart-container-demo cat > app.py << 'EOF' import json, os, time, uuid # Simulated expensive init work — model load, config fetch, etc. _INIT_START = time.time() time.sleep(2.0) # stand-in for loading a model or large dataset _INIT_DURATION = time.time() - _INIT_START # DANGER: generated once at init, frozen into the snapshot. # Must be regenerated in afterRestore — see Step 6. request_id_seed = str(uuid.uuid4()) def handler(event, context): return { "statusCode": 200, "body": json.dumps({ "init_duration_sec": round(_INIT_DURATION, 2), "seed_at_init": request_id_seed, "invoke_time": time.time() }) } EOF -
Write the Dockerfile using an AWS base image.
cat > Dockerfile << 'EOF' FROM public.ecr.aws/lambda/python:3.12 COPY app.py ${LAMBDA_TASK_ROOT} CMD [ "app.handler" ] EOF -
Build the image and push it to ECR.
aws ecr create-repository --repository-name snapstart-container-demo aws ecr get-login-password --region us-east-1 | \ docker login --username AWS --password-stdin \ ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com docker build -t snapstart-container-demo . docker tag snapstart-container-demo:latest \ ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/snapstart-container-demo:latest docker push ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/snapstart-container-demo:latest -
Create the Lambda function from the image. SnapStart requires a published version, so create the function first.
aws lambda create-function \ --function-name snapstart-container-demo \ --package-type Image \ --code ImageUri=ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/snapstart-container-demo:latest \ --role arn:aws:iam::ACCOUNT_ID:role/lambda-basic-execution \ --timeout 15 \ --memory-size 1024 -
Test cold-start latency before SnapStart. Publish a version and invoke it cold (a fresh version has no warm environments yet).
aws lambda publish-version --function-name snapstart-container-demo time aws lambda invoke \ --function-name snapstart-container-demo:1 \ --cli-binary-format raw-in-base64-out \ out.json && cat out.jsonYou should see a response time in the multi-second range — mostly the 2-second simulated init plus image/runtime startup overhead.
-
Fix uniqueness before enabling SnapStart. The
request_id_seedfrom Step 1 is generated once at init and would otherwise be frozen into every restored environment. Add runtime hooks so it's regenerated after every restore, using the Lambda SnapStart runtime hooks API.cat > app.py << 'EOF' import json, os, time, uuid _INIT_START = time.time() time.sleep(2.0) _INIT_DURATION = time.time() - _INIT_START request_id_seed = str(uuid.uuid4()) # Runtime hooks: import triggers registration with the Lambda # runtime client. Regenerate anything unique here. from lambda_runtime_client import LambdaRuntimeClient # provided by base image runtime def after_restore(): global request_id_seed request_id_seed = str(uuid.uuid4()) def handler(event, context): return { "statusCode": 200, "body": json.dumps({ "init_duration_sec": round(_INIT_DURATION, 2), "seed_at_init": request_id_seed, "invoke_time": time.time() }) } EOFRebuild and push the updated image following Step 3 before continuing.
-
Enable SnapStart on the function.
aws lambda update-function-configuration \ --function-name snapstart-container-demo \ --snap-start ApplyOn=PublishedVersions -
Publish a new version and invoke it. The first invoke after publishing still triggers the full init-and-snapshot cycle — that cost moves to publish time, not invoke time.
aws lambda publish-version --function-name snapstart-container-demo time aws lambda invoke \ --function-name snapstart-container-demo:2 \ --cli-binary-format raw-in-base64-out \ out.json && cat out.json -
Test cold-start latency after SnapStart. Force a fresh execution environment by invoking again after a few minutes of no traffic (or use a new version), and compare.
time aws lambda invoke \ --function-name snapstart-container-demo:2 \ --cli-binary-format raw-in-base64-out \ out.json && cat out.jsonCompare the
seed_at_initvalues across multiple cold invokes — with theafter_restorehook in place, each one should be different, confirming uniqueness is handled correctly instead of silently reusing frozen state.

Why the Order of Operations Matters
Notice that Step 6 — fixing uniqueness — happens before Step 7, actually enabling SnapStart. That ordering isn't arbitrary. If you flip SnapStart on before you've audited what your init phase generates, you can end up publishing a version whose frozen state is already leaking across restored environments before you've noticed anything is wrong, because the function still returns 200 and the response still looks valid. The bug only shows up when two concurrent cold invokes compare notes and find they got the same "unique" ID. Treat the uniqueness pass as a required step before enabling SnapStart on any function you didn't write from scratch with this in mind, not an optional hardening step you can circle back to later.
Verifying the Latency Gain
Check the Init Duration field in CloudWatch Logs for both versions — you can query it directly with CloudWatch Logs Insights:
fields @timestamp, @initDuration, @billedDuration
| filter @type = "REPORT"
| sort @timestamp desc
| limit 20
Version 1 (no SnapStart) should show init duration in the seconds range on every cold start. Version 2 (SnapStart enabled) should show a dramatically shorter init duration on cold starts after the first publish, because it's restoring from a snapshot instead of re-running your init code. This is the exact same behavior AWS documents for managed runtimes, now available for container images — see the SnapStart overview and the container image announcement for the underlying details.
Troubleshooting Common Issues
- SnapStart update rejected with an unsupported architecture or runtime error. Double-check your base image — SnapStart for container images requires an AWS-provided base image from the Lambda container image gallery or a custom image that correctly implements the Lambda Runtime API. A generic
python:3.12-slimimage without the Lambda Runtime Interface Client won't work. - First invoke after enabling SnapStart is still slow. That's expected. Enabling SnapStart doesn't retroactively snapshot anything — the next
publish-versioncall triggers a fresh init-and-snapshot cycle. Only invokes against that new published version benefit;$LATESTis never eligible for SnapStart. - Duplicate values showing up in
seed_at_initacross separate cold invokes. This means yourafterRestorehook either isn't registered correctly or isn't being called before the handler runs. Check that your runtime-hooks registration happens at module import time, before the handler function is defined, and confirm your base image version supports the hooks API — older base image tags predate this feature. - Restore duration looks high in CloudWatch. Look at what your
afterRestorehook is doing. Reopening a database connection, re-authenticating to an external service, or regenerating a large amount of cryptographic material all add real time to the restore path — that cost is now visible instead of hidden inside a generic cold start.
Cleaning Up
Once you're done experimenting, remove the resources you created so you're not paying for an idle repository and function:
aws lambda delete-function --function-name snapstart-container-demo
aws ecr delete-repository \
--repository-name snapstart-container-demo \
--force
What You Built
You built a container image Lambda function, measured its cold-start behavior before and after enabling SnapStart, and — critically — fixed the uniqueness problem that trips up most first attempts at this, by regenerating a frozen init-time value on every restore instead of letting it leak across execution environments. That pattern (identify what's frozen at init, decide what needs regenerating, wire it into an afterRestore hook) is the actual engineering work behind adopting SnapStart on anything real: a database connection, a signed token, a cryptographic nonce. Apply the same checklist there before you flip the switch in production.
At Enkompass (enkompass.net), this is our home turf. We build event-driven, serverless systems that scale to zero and bill like it. We've walked more than one team through exactly this uniqueness fix after a SnapStart rollout produced duplicate session tokens in production. If you'd rather have seasoned cloud people handle it instead of learning the hard way at 2 AM, let's talk. Reach out through our contact page or call us directly at +1-828-436-5667 — we'll help you turn this from a someday-project into a done-deal.
