Free .NET Web API Course

AWS Lambda SnapStart for .NET – Cut Cold Starts by 90% (With Benchmarks)

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

11 min read

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.

Thank You, AWS! 🚀

This article is sponsored by AWS. Huge thanks for helping me produce more .NET on AWS content!

Sponsored Content

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.

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

<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:

Terminal window
dotnet new lambda.EmptyFunction -n SnapStartDemo
cd SnapStartDemo/src/SnapStartDemo

Replace 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:

Terminal window
dotnet lambda deploy-function SnapStartDemo --function-runtime dotnet8 --function-memory-size 512 --function-timeout 30

Measure Baseline Cold Starts

Invoke the function and check CloudWatch Logs:

Terminal window
aws lambda invoke --function-name SnapStartDemo --payload '{}' response.json
cat response.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
aws lambda update-function-configuration --function-name SnapStartDemo --snap-start ApplyOn=PublishedVersions
# Wait for update to complete
aws 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:

  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

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

Terminal window
aws lambda create-alias --function-name SnapStartDemo --name live --function-version 1

Now invoke via the alias:

Terminal window
aws lambda invoke --function-name SnapStartDemo: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
Improvement87% 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 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:

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 start
private static readonly string CorrelationId = Guid.NewGuid().ToString();
// GOOD: Generate after restore
private 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 expired

Solution: 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:

Terminal window
# These work with SnapStart
aws lambda invoke --function-name MyFunc:live ...
aws lambda invoke --function-name MyFunc:1 ...
# This does NOT use SnapStart
aws lambda invoke --function-name MyFunc ...

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 :)

What's your Feedback?
Do let me know your thoughts around this article.

Level Up Your .NET Skills

Join 8,000+ developers. Get one practical tip each week with best practices and real-world examples.

Weekly tips
Code examples
100% free
No spam, unsubscribe anytime