VOOZH about

URL: https://codewithmukesh.com/blog/lambda-snapstart-dotnet/

⇱ AWS Lambda SnapStart for .NET – Cut Cold Starts Significantly - codewithmukesh


Skip to main content
Article complete

Get one like this every Tuesday at 7 PM IST.

Back to blog
dotnet aws 11 min read Lesson 12/57

AWS Lambda SnapStart for .NET – Cut Cold Starts Significantly

Stop losing users to Lambda cold starts. Learn how to enable SnapStart for .NET functions, use runtime hooks for optimization, and see real benchmark data showing 58-94% faster cold starts.

Stop losing users to Lambda cold starts. Learn how to enable SnapStart for .NET functions, use runtime hooks for optimization, and see real benchmark data showing 58-94% faster cold starts.

dotnet aws

lambda snapstart cold-starts serverless performance

👁 Mukesh Murugan
Mukesh Murugan
Software Engineer
Chapter · 12 of 57 Module 2 of 15 Free
View course

AWS for .NET Developers

From dotnet new to docker push — REST, EF Core 10, auth, caching, Clean Architecture, observability. 57 hands-on lessons, source on GitHub.

Every .NET developer who’s built serverless APIs on Lambda knows the pain: your function works great once it’s warm, but that first request after idle time? 2-6 seconds of cold start latency while .NET loads assemblies, runs JIT compilation, and initializes your dependency injection container.

For user-facing APIs, that’s unacceptable. Users don’t wait 5 seconds for a response - they leave.

AWS Lambda SnapStart changes everything. Instead of re-initializing your function on every cold start, Lambda takes a snapshot of your fully initialized execution environment and restores it in milliseconds. Real-world results show 58-94% reduction in cold start times.

In this article, we’ll build a .NET 8 Lambda function, enable SnapStart, implement runtime hooks for optimization, and measure the actual performance improvement. You’ll see the before/after numbers and learn the gotchas that can trip you up.

What is Lambda SnapStart?

Lambda SnapStart works by:

  1. Initializing your function once when you publish a new version
  2. Taking an encrypted snapshot of the execution environment (memory + disk state)
  3. Restoring from that snapshot on subsequent cold starts instead of re-initializing

Instead of paying the JIT compilation and DI initialization cost on every cold start, you pay it once at publish time. Cold starts become snapshot restores - typically single-digit milliseconds for the restore operation itself.

Why .NET Benefits Significantly

Here is the truth, .NET Lambda functions have notoriously slow cold starts. These are the main culprits:

  • JIT Compilation: The runtime compiles IL to native code on first execution - can take several seconds
  • Reflection-Heavy DI: Building ServiceCollection with large dependency graphs is expensive
  • Assembly Loading: Loading dozens of NuGet packages takes time

SnapStart addresses all of these by capturing the post-initialization state. Your second cold start skips all that work.

Real Performance Numbers

Here’s what AWS and the community have measured:

MetricWithout SnapStartWith SnapStartImprovement
P90 Cold Start1,680 ms698 ms58% faster
Heavy Init (5.5s)5,550 ms~350 ms94% faster
Typical .NET API2,000-4,000 ms300-700 ms70-85% faster

Combined with Native AOT: If you use SnapStart with Native AOT, cold starts become competitive with interpreted languages while using 52% less memory.

Read nextCompanion article

Essential AWS Services for .NET Developers

New to AWS? Start with this guide covering the core services every .NET developer should know.

Prerequisites

  • .NET 8 SDK – SnapStart supports .NET 8 and later
  • AWS Account with Lambda permissions
  • AWS CLI v2Install here
  • Docker – For local testing (optional)

Required NuGet Packages

<PackageReferenceInclude="Amazon.Lambda.Core"Version="2.5.0" />
<PackageReferenceInclude="Amazon.Lambda.Serialization.SystemTextJson"Version="2.4.4" />

For runtime hooks (SnapStart optimization):

<PackageReferenceInclude="Amazon.Lambda.Core"Version="2.5.0" />

The SnapshotRestore class for runtime hooks is included in Amazon.Lambda.Core version 2.5.0+.

Step 1: Create the Lambda Function

Let’s create a function that simulates realistic initialization - loading configuration, setting up DI, and warming up SDK clients.

Create a new Lambda project:

Terminal window
dotnetnewlambda.EmptyFunction-nSnapStartDemo
cdSnapStartDemo/src/SnapStartDemo

Replace Function.cs with:

usingSystem.Diagnostics;
usingSystem.Text.Json;
usingAmazon.Lambda.Core;
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespaceSnapStartDemo;
publicclassFunction
{
privatestaticreadonlyStopwatchInitStopwatch;
privatestaticreadonlyDateTimeOffsetInitTime;
privatestaticreadonlystringEnvironmentId;
privatestaticbool_isFirstInvocation=true;
staticFunction()
{
InitTime=DateTimeOffset.UtcNow;
InitStopwatch=Stopwatch.StartNew();
EnvironmentId=Guid.NewGuid().ToString("N")[..8];
// Simulate realistic initialization work
// In real apps: DI container build, config loading, SDK client warmup
SimulateHeavyInitialization();
InitStopwatch.Stop();
}
privatestaticvoidSimulateHeavyInitialization()
{
// Simulate loading configuration
Thread.Sleep(500);
// Simulate building DI container with reflection
Thread.Sleep(800);
// Simulate SDK client initialization
Thread.Sleep(700);
// Total: ~2 seconds of simulated init work
}
publicasyncTask<string> FunctionHandler(objectinput, ILambdaContextcontext)
{
varrequestStart=DateTimeOffset.UtcNow;
varisFirstInvocation=_isFirstInvocation;
_isFirstInvocation=false;
// Log cold start information
context.Logger.LogInformation(
"RequestId={RequestId} | ColdStart={ColdStart} | InitMs={InitMs} | EnvId={EnvId}",
context.AwsRequestId,
isFirstInvocation,
InitStopwatch.ElapsedMilliseconds,
EnvironmentId
);
// Simulate actual work
awaitTask.Delay(50);
returnJsonSerializer.Serialize(new
{
Message="Hello from SnapStart Demo!",
ColdStart=isFirstInvocation,
InitDurationMs=InitStopwatch.ElapsedMilliseconds,
EnvironmentId=EnvironmentId,
RequestTime=requestStart.ToString("O")
});
}
}

What This Code Does

  • Static constructor: Runs once per execution environment, simulating 2 seconds of initialization
  • EnvironmentId: A unique ID generated at init time - useful for demonstrating SnapStart behavior
  • _isFirstInvocation: Tracks whether this is the first request in this environment

Step 2: Deploy Without SnapStart (Baseline)

First, let’s deploy without SnapStart to establish a baseline.

I’ve covered Lambda deployment in detail in a previous article, so I won’t repeat everything here.

Read nextCompanion article

AWS Lambda with .NET 6 - Getting Started with Serverless Computing

Complete guide covering AWS CLI setup, Lambda templates, debugging locally, and deploying to AWS.

The easiest way to deploy is directly from Visual Studio using the AWS Toolkit extension. Right-click your project → Publish to AWS Lambda → follow the wizard. It handles everything - building, packaging, creating IAM roles, and uploading.

For CLI deployment, use the dotnet lambda deploy-function command:

Terminal window
dotnetlambdadeploy-functionSnapStartDemo--function-runtimedotnet8--function-memory-size512--function-timeout30

Measure Baseline Cold Starts

Invoke the function and check CloudWatch Logs:

Terminal window
awslambdainvoke--function-nameSnapStartDemo--payload'{}'response.json
catresponse.json

In CloudWatch, you’ll see the REPORT line:

REPORT RequestId: abc123 Init Duration: 2001.00 ms Duration: 55.00 ms Billed Duration: 2206 ms

And here is the CLI response:

{
"Message": "Hello from SnapStart Demo!",
"ColdStart": true,
"InitDurationMs": 2001,
"EnvironmentId": "66b14194",
"RequestTime": "2025-12-29T14:55:38.0284483"
}

That Init Duration of ~2001ms is our baseline cold start. Invoke a few more times quickly to confirm warm invocations show no Init Duration.

Step 3: Enable SnapStart

SnapStart only works on published versions - not $LATEST. Here’s the process:

Via AWS Console

  1. Open your Lambda function in the AWS Console
  2. Go to ConfigurationGeneral configuration
  3. Click Edit
  4. Under SnapStart, select PublishedVersions
  5. Save changes
  6. Go to Versions tab → Publish new version

Via AWS CLI

Terminal window
# Enable SnapStart
awslambdaupdate-function-configuration--function-nameSnapStartDemo--snap-startApplyOn=PublishedVersions
# Wait for update to complete
awslambdawaitfunction-updated--function-nameSnapStartDemo
# Publish a new version (this triggers snapshot creation)
awslambdapublish-version--function-nameSnapStartDemo--description"SnapStart enabled"

When you publish the version, AWS:

  1. Initializes your function (runs the static constructor)
  2. Takes a snapshot of the execution environment
  3. Stores the encrypted snapshot for future cold starts

Create an Alias (Recommended)

Don’t invoke version numbers directly - use an alias:

Terminal window
awslambdacreate-alias--function-nameSnapStartDemo--namelive--function-version1

Now invoke via the alias:

Terminal window
awslambdainvoke--function-nameSnapStartDemo:live--payload'{}'response.json

Step 4: Measure SnapStart Performance

After enabling SnapStart, invoke the function and check CloudWatch:

REPORT RequestId: xyz789 Restore Duration: 285.00 ms Duration: 52.00 ms Billed Duration: 337 ms

Notice the difference:

MetricWithout SnapStartWith SnapStart
Init/Restore Duration2,150 ms285 ms
Improvement-87% faster

The Init Duration is replaced by Restore Duration - the time to restore from the snapshot.

Step 5: Optimize with Runtime Hooks

SnapStart provides runtime hooks to customize what happens before the snapshot and after restoration. This is where you can squeeze out even more performance.

The Runtime Hooks API

usingAmazon.Lambda.Core;
// Register code to run BEFORE snapshot is taken
Amazon.Lambda.Core.SnapshotRestore.RegisterBeforeSnapshot(async () =>
{
// This runs once when the version is published
// Good for: JIT warmup, preloading data, building caches
});
// Register code to run AFTER snapshot is restored
Amazon.Lambda.Core.SnapshotRestore.RegisterAfterRestore(async () =>
{
// This runs on every cold start after restore
// Good for: Refreshing credentials, re-establishing connections
});

Optimized Function with Runtime Hooks

Here’s an enhanced version that uses runtime hooks:

usingSystem.Diagnostics;
usingSystem.Text.Json;
usingAmazon.Lambda.Core;
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespaceSnapStartDemo;
publicclassFunction
{
privatestaticreadonlyStopwatchInitStopwatch;
privatestaticreadonlyDateTimeOffsetInitTime;
privatestaticstring_environmentId;
privatestaticbool_isFirstInvocation=true;
privatestaticbool_isRestoredFromSnapshot=false;
staticFunction()
{
InitTime=DateTimeOffset.UtcNow;
InitStopwatch=Stopwatch.StartNew();
_environmentId=Guid.NewGuid().ToString("N")[..8];
// Simulate initialization
SimulateHeavyInitialization();
// Register SnapStart hooks
RegisterSnapStartHooks();
InitStopwatch.Stop();
}
privatestaticvoidRegisterSnapStartHooks()
{
// BEFORE SNAPSHOT: Warm up JIT and preload data
SnapshotRestore.RegisterBeforeSnapshot(async () =>
{
Console.WriteLine("[SnapStart] Running pre-snapshot warmup...");
// Trigger tiered JIT compilation by invoking hot paths
for (inti=0; i<10; i++)
{
_=JsonSerializer.Serialize(new { Test=i });
_=JsonSerializer.Deserialize<Dictionary<string, object>>("{}");
}
// Preload any static data or configuration
awaitTask.CompletedTask;
Console.WriteLine("[SnapStart] Pre-snapshot warmup complete.");
});
// AFTER RESTORE: Refresh anything that shouldn't be frozen
SnapshotRestore.RegisterAfterRestore(async () =>
{
Console.WriteLine("[SnapStart] Running post-restore initialization...");
_isRestoredFromSnapshot=true;
// Generate a NEW unique ID for this restored environment
// The original _environmentId is frozen in the snapshot
_environmentId=$"restored-{Guid.NewGuid().ToString("N")[..8]}";
// Refresh credentials, re-establish connections, etc.
// Example: await RefreshSecretsAsync();
awaitTask.CompletedTask;
Console.WriteLine("[SnapStart] Post-restore initialization complete.");
});
}
privatestaticvoidSimulateHeavyInitialization()
{
Thread.Sleep(500); // Config loading
Thread.Sleep(800); // DI container
Thread.Sleep(700); // SDK clients
}
publicasyncTask<string> FunctionHandler(objectinput, ILambdaContextcontext)
{
varisFirstInvocation=_isFirstInvocation;
_isFirstInvocation=false;
context.Logger.LogInformation(
"RequestId={RequestId} | ColdStart={ColdStart} | Restored={Restored} | InitMs={InitMs} | EnvId={EnvId}",
context.AwsRequestId,
isFirstInvocation,
_isRestoredFromSnapshot,
InitStopwatch.ElapsedMilliseconds,
_environmentId
);
awaitTask.Delay(50);
returnJsonSerializer.Serialize(new
{
Message="Hello from SnapStart Demo!",
ColdStart=isFirstInvocation,
RestoredFromSnapshot=_isRestoredFromSnapshot,
InitDurationMs=InitStopwatch.ElapsedMilliseconds,
EnvironmentId=_environmentId
});
}
}

What the Hooks Do

RegisterBeforeSnapshot (runs once at publish time):

  • ✅ Trigger JIT compilation for hot paths
  • ✅ Preload static configuration
  • ✅ Build and resolve DI container
  • ✅ Warm up serialization

RegisterAfterRestore (runs on every cold start):

  • ✅ Regenerate unique IDs (frozen in snapshot otherwise)
  • ✅ Refresh credentials and secrets
  • ✅ Re-establish network connections
  • ✅ Update timestamps

Time Limits

  • Before Snapshot: 130 seconds (or function timeout, whichever is higher)
  • After Restore: 10 seconds maximum – keep it fast or you’ll get SnapStartTimeoutException

Critical Gotchas

SnapStart changes your function’s lifecycle. Here’s what can bite you:

1. Uniqueness is Frozen

Problem: The same snapshot is used for ALL execution environments. Anything generated during initialization is identical across all instances.

// BAD: This GUID will be the same for every cold start
privatestaticreadonlystringCorrelationId=Guid.NewGuid().ToString();
// GOOD: Generate after restore
privatestaticstring_correlationId;
staticFunction()
{
SnapshotRestore.RegisterAfterRestore(async () =>
{
_correlationId=Guid.NewGuid().ToString();
awaitTask.CompletedTask;
});
}

This affects:

  • GUIDs and random values
  • Timestamps captured at init
  • Machine-specific identifiers

2. Secrets Manager Extension is INCOMPATIBLE

Problem: The AWS Secrets Manager Lambda extension generates credentials during initialization. With SnapStart, those credentials are frozen and will expire.

Error: The security token included in the request is expired

Solution: Fetch secrets in RegisterAfterRestore or use the SDK directly with credential refresh:

SnapshotRestore.RegisterAfterRestore(async () =>
{
// Refresh secrets after restore
varsecretsClient=newAmazonSecretsManagerClient();
varsecret=awaitsecretsClient.GetSecretValueAsync(newGetSecretValueRequest
{
SecretId="my-secret"
});
_cachedSecret=secret.SecretString;
});

3. Network Connections May Be Stale

Problem: TCP connections opened during initialization may not survive snapshot/restore.

Solution: Use connection pooling and validate connections after restore:

SnapshotRestore.RegisterAfterRestore(async () =>
{
// Validate or re-establish connections
await_httpClient.GetAsync("https://health.check/ping");
});

Most AWS SDK clients handle this automatically, but custom connections need attention.

4. Only Published Versions

Problem: SnapStart doesn’t work on $LATEST.

Solution: Always invoke via version number or alias:

Terminal window
# These work with SnapStart
awslambdainvoke--function-nameMyFunc:live...
awslambdainvoke--function-nameMyFunc:1...
# This does NOT use SnapStart
awslambdainvoke--function-nameMyFunc...

5. Provisioned Concurrency is Incompatible

You cannot use SnapStart and Provisioned Concurrency together. Choose one:

FeatureSnapStartProvisioned Concurrency
Cold Start Reduction58-94%100% (always warm)
CostCache + restore feesHigh (always-on billing)
Best ForVariable trafficConsistent high traffic

Pricing

Unlike Java (where SnapStart is free), .NET SnapStart has costs:

Snapshot Caching

  • Rate: $0.0000015046 per GB-second
  • Minimum: 3 hours per published version
  • Charged for memory allocated × time cached

Snapshot Restoration

  • Rate: $0.0001397998 per GB
  • Charged each time Lambda restores from snapshot

Example Cost

For a 1024 MB function running 24/7 with 2,400 cold starts/day:

ComponentCalculationMonthly Cost
Cache1 GB × 2,678,400 seconds$4.03
Restores74,400 restores × 1 GB$10.40
Total SnapStart Cost$14.43

Compare this to Provisioned Concurrency which could cost $50-100+/month for similar traffic. SnapStart is significantly cheaper for most workloads.

Cost Optimization Tips:

  • Delete old unused versions (each published version is cached)
  • Right-size memory allocation
  • Monitor CloudWatch to ensure benefits outweigh costs

When NOT to Use SnapStart

Skip SnapStart if:

  • ❌ Init duration is already <500ms (not worth the complexity)
  • ❌ Function is invoked infrequently (snapshot may expire)
  • ❌ You need Provisioned Concurrency
  • ❌ You’re using EFS or >512 MB ephemeral storage
  • ❌ You’re using the Secrets Manager extension

SnapStart vs Native AOT vs Provisioned Concurrency

FeatureSnapStartNative AOTProvisioned Concurrency
Cold Start Reduction58-94%~75%100%
Code ChangesMinimal (hooks)Significant (source generators)None
Memory UsageNormal52% lessNormal
CostLow (cache + restore)NoneHigh
CompatibilityAll .NET 8+ codeAOT-compatible onlyAll code
Can Combine✅ With AOT✅ With SnapStart

Recommendation: For new projects, Native AOT + SnapStart delivers the best cold start performance. For existing projects, SnapStart alone is the lowest-effort improvement.

Limitations

SnapStart doesn’t support:

  • $LATEST version (only published versions)
  • ❌ Provisioned Concurrency
  • ❌ Amazon EFS
  • ❌ Ephemeral storage >512 MB
  • ❌ Container images (only managed runtimes)
  • ❌ ARM64 (x86_64 only as of now)

Regional Availability

Available in all commercial AWS regions except:

  • Asia Pacific (New Zealand)
  • Asia Pacific (Taipei)

Troubleshooting

SnapStartTimeoutException

Your RegisterAfterRestore hook is taking too long.

SnapStartTimeoutException: Function timed out while restoring snapshot

Fix: Keep after-restore hooks under 10 seconds. Move heavy work to before-snapshot.

Expired Credentials

The security token included in the request is expired

Fix: Don’t use Secrets Manager extension. Fetch secrets in RegisterAfterRestore.

Same EnvironmentId Across Instances

Your unique IDs are frozen in the snapshot.

Fix: Generate unique values in RegisterAfterRestore, not in static constructors.

Wrap-Up

Lambda SnapStart is a game-changer for .NET serverless applications. With minimal code changes, you can reduce cold starts by 58-94% and dramatically improve user experience for latency-sensitive APIs.

Key takeaways:

  • Enable on published versions – SnapStart doesn’t work on $LATEST
  • Use runtime hooksRegisterBeforeSnapshot for warmup, RegisterAfterRestore for freshness
  • Watch for uniqueness issues – GUIDs and timestamps are frozen
  • Avoid Secrets Manager extension – Fetch secrets in restore hooks instead
  • Consider combining with Native AOT – Best of both worlds

The pattern we built - runtime hooks for optimization with proper uniqueness handling - gives you production-ready SnapStart that actually works.

Grab the complete source code from github.com/iammukeshm/lambda-snapstart-dotnet and start cutting those cold starts today.

Have questions or run into issues? Drop a comment below - I’d love to hear how SnapStart is working for your .NET Lambdas.

Happy Coding :)

More from the archive.

View all articles

What's your take?

Push back, share a war story, or ask the obvious question someone else is wondering. I read every comment.

View on GitHub

Weekly .NET tips · free

Subscribed · Tue 7 PM IST

You're in.
Welcome to the crew.

Tuesday's issue lands in your inbox at 7 PM IST. One last step: confirm your email so it actually arrives.

01 · Check your inbox

02 · Every Tuesday

Benchmarks, architecture insights, and production tips that never make it to the blog.

Privacy notice · 30s read

Cookies, but only the useful ones.

I use cookies to understand which articles get read and which CTAs actually work. No third-party advertising trackers, ever. Read the privacy policy →