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.
This article is sponsored by AWS. Huge thanks for helping me produce more .NET on AWS content!
What is Lambda SnapStart?
Lambda SnapStart works by:
- Initializing your function once when you publish a new version
- Taking an encrypted snapshot of the execution environment (memory + disk state)
- 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
ServiceCollectionwith 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:
| Metric | Without SnapStart | With SnapStart | Improvement |
|---|---|---|---|
| P90 Cold Start | 1,680 ms | 698 ms | 58% faster |
| Heavy Init (5.5s) | 5,550 ms | ~350 ms | 94% faster |
| Typical .NET API | 2,000-4,000 ms | 300-700 ms | 70-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.
Prerequisites
- .NET 8 SDK – SnapStart supports .NET 8 and later
- AWS Account with Lambda permissions
- AWS CLI v2 – Install here
- Docker – For local testing (optional)
Required NuGet Packages
<PackageReference Include="Amazon.Lambda.Core" Version="2.5.0" /><PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.4.4" />For runtime hooks (SnapStart optimization):
<PackageReference Include="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:
dotnet new lambda.EmptyFunction -n SnapStartDemocd SnapStartDemo/src/SnapStartDemoReplace Function.cs with:
using System.Diagnostics;using System.Text.Json;using Amazon.Lambda.Core;
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace SnapStartDemo;
public class Function{ private static readonly Stopwatch InitStopwatch; private static readonly DateTimeOffset InitTime; private static readonly string EnvironmentId; private static bool _isFirstInvocation = true;
static Function() { 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(); }
private static void SimulateHeavyInitialization() { // 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 }
public async Task<string> FunctionHandler(object input, ILambdaContext context) { var requestStart = DateTimeOffset.UtcNow; var isFirstInvocation = _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 await Task.Delay(50);
return JsonSerializer.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.
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:
dotnet lambda deploy-function SnapStartDemo --function-runtime dotnet8 --function-memory-size 512 --function-timeout 30Measure Baseline Cold Starts
Invoke the function and check CloudWatch Logs:
aws lambda invoke --function-name SnapStartDemo --payload '{}' response.jsoncat response.jsonIn CloudWatch, you’ll see the REPORT line:
REPORT RequestId: abc123 Init Duration: 2001.00 ms Duration: 55.00 ms Billed Duration: 2206 msAnd 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
- Open your Lambda function in the AWS Console
- Go to Configuration → General configuration
- Click Edit
- Under SnapStart, select PublishedVersions
- Save changes
- Go to Versions tab → Publish new version
Via AWS CLI
# Enable SnapStartaws lambda update-function-configuration --function-name SnapStartDemo --snap-start ApplyOn=PublishedVersions
# Wait for update to completeaws lambda wait function-updated --function-name SnapStartDemo
# Publish a new version (this triggers snapshot creation)aws lambda publish-version --function-name SnapStartDemo --description "SnapStart enabled"When you publish the version, AWS:
- Initializes your function (runs the static constructor)
- Takes a snapshot of the execution environment
- Stores the encrypted snapshot for future cold starts
Create an Alias (Recommended)
Don’t invoke version numbers directly—use an alias:
aws lambda create-alias --function-name SnapStartDemo --name live --function-version 1Now invoke via the alias:
aws lambda invoke --function-name SnapStartDemo:live --payload '{}' response.jsonStep 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 msNotice the difference:
| Metric | Without SnapStart | With SnapStart |
|---|---|---|
| Init/Restore Duration | 2,150 ms | 285 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
using Amazon.Lambda.Core;
// Register code to run BEFORE snapshot is takenAmazon.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 restoredAmazon.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:
using System.Diagnostics;using System.Text.Json;using Amazon.Lambda.Core;
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace SnapStartDemo;
public class Function{ private static readonly Stopwatch InitStopwatch; private static readonly DateTimeOffset InitTime; private static string _environmentId; private static bool _isFirstInvocation = true; private static bool _isRestoredFromSnapshot = false;
static Function() { InitTime = DateTimeOffset.UtcNow; InitStopwatch = Stopwatch.StartNew(); _environmentId = Guid.NewGuid().ToString("N")[..8];
// Simulate initialization SimulateHeavyInitialization();
// Register SnapStart hooks RegisterSnapStartHooks();
InitStopwatch.Stop(); }
private static void RegisterSnapStartHooks() { // 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 (int i = 0; i < 10; i++) { _ = JsonSerializer.Serialize(new { Test = i }); _ = JsonSerializer.Deserialize<Dictionary<string, object>>("{}"); }
// Preload any static data or configuration await Task.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();
await Task.CompletedTask;
Console.WriteLine("[SnapStart] Post-restore initialization complete."); }); }
private static void SimulateHeavyInitialization() { Thread.Sleep(500); // Config loading Thread.Sleep(800); // DI container Thread.Sleep(700); // SDK clients }
public async Task<string> FunctionHandler(object input, ILambdaContext context) { var isFirstInvocation = _isFirstInvocation; _isFirstInvocation = false;
context.Logger.LogInformation( "RequestId={RequestId} | ColdStart={ColdStart} | Restored={Restored} | InitMs={InitMs} | EnvId={EnvId}", context.AwsRequestId, isFirstInvocation, _isRestoredFromSnapshot, InitStopwatch.ElapsedMilliseconds, _environmentId );
await Task.Delay(50);
return JsonSerializer.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 startprivate static readonly string CorrelationId = Guid.NewGuid().ToString();
// GOOD: Generate after restoreprivate static string _correlationId;
static Function(){ SnapshotRestore.RegisterAfterRestore(async () => { _correlationId = Guid.NewGuid().ToString(); await Task.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 expiredSolution: Fetch secrets in RegisterAfterRestore or use the SDK directly with credential refresh:
SnapshotRestore.RegisterAfterRestore(async () =>{ // Refresh secrets after restore var secretsClient = new AmazonSecretsManagerClient(); var secret = await secretsClient.GetSecretValueAsync(new GetSecretValueRequest { 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:
# These work with SnapStartaws lambda invoke --function-name MyFunc:live ...aws lambda invoke --function-name MyFunc:1 ...
# This does NOT use SnapStartaws lambda invoke --function-name MyFunc ...5. Provisioned Concurrency is Incompatible
You cannot use SnapStart and Provisioned Concurrency together. Choose one:
| Feature | SnapStart | Provisioned Concurrency |
|---|---|---|
| Cold Start Reduction | 58-94% | 100% (always warm) |
| Cost | Cache + restore fees | High (always-on billing) |
| Best For | Variable traffic | Consistent 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:
| Component | Calculation | Monthly Cost |
|---|---|---|
| Cache | 1 GB × 2,678,400 seconds | $4.03 |
| Restores | 74,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
| Feature | SnapStart | Native AOT | Provisioned Concurrency |
|---|---|---|---|
| Cold Start Reduction | 58-94% | ~75% | 100% |
| Code Changes | Minimal (hooks) | Significant (source generators) | None |
| Memory Usage | Normal | 52% less | Normal |
| Cost | Low (cache + restore) | None | High |
| Compatibility | All .NET 8+ code | AOT-compatible only | All 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:
- ❌
$LATESTversion (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 snapshotFix: Keep after-restore hooks under 10 seconds. Move heavy work to before-snapshot.
Expired Credentials
The security token included in the request is expiredFix: 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 hooks –
RegisterBeforeSnapshotfor warmup,RegisterAfterRestorefor 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 :)


