Serverless applications are attractive because infrastructure management is reduced and workloads can scale without continuously running application servers.
For .NET developers, however, one performance characteristic deserves special attention: cold starts.
A cold start occurs when a new execution environment must be initialized before a function can process a request.
For a simple function, the delay may be relatively easy to understand:
Request
|
v
Create Environment
|
v
Start .NET Runtime
|
v
Load Application
|
v
Execute Function
|
v
Response
Durable workflows make the problem more interesting.
A workflow may invoke several function executions:
Workflow
|
+--> Function A
|
+--> Function B
|
+--> Function C
|
+--> Function D
Each execution can experience different startup behavior.
This article explains how to benchmark .NET Lambda cold starts when functions participate in durable workflows, what measurements matter, how to design a repeatable test, and which optimizations are worth evaluating.
What Is a Cold Start?
A cold start happens when the platform needs to create a new execution environment before processing a function invocation.
A simplified execution path is:
Incoming Request
|
v
Environment Available?
|
+--+--+
| |
Yes No
| |
| v
| Initialize
| |
| v
+--> .NET Runtime
|
v
Application
|
v
Handler
A warm invocation skips much of the initialization work:
Request
|
v
Existing Environment
|
v
Handler
|
v
Response
This creates two different latency measurements:
Cold Invocation
---------------
Initialization + Execution
Warm Invocation
---------------
Execution
Both should be measured separately.
Why Durable Workflows Change the Benchmark
A normal serverless benchmark may measure:
Request -> Function -> Response
A durable workflow benchmark should measure:
Workflow Start
|
v
Activity A
|
v
Activity B
|
v
Activity C
|
v
Workflow Complete
The workflow adds additional factors:
Scheduling delay
State persistence
Function initialization
Function execution
Serialization
Deserialization
Retry behavior
Environment reuse
Therefore, measuring only the first function invocation does not provide a complete picture.
What Should Be Measured?
A useful benchmark should capture several dimensions.
| Metric | Purpose |
|---|
| Cold-start latency | Measures initialization overhead |
| Warm latency | Establishes execution baseline |
| Workflow duration | Measures end-to-end behavior |
| Initialization time | Identifies startup cost |
| Handler execution time | Separates application work |
| Memory usage | Helps evaluate resource configuration |
| Retry count | Shows reliability behavior |
| Activity latency | Identifies slow workflow steps |
| Environment reuse | Helps understand warm execution |
| Concurrent execution latency | Measures scaling behavior |
The objective is not to produce one impressive number.
The objective is to understand where the time is being spent.
Separate Initialization From Business Logic
A function can be slow because of application initialization rather than business logic.
For example:
public class OrderFunction
{
private readonly OrderService _service;
public OrderFunction()
{
_service = new OrderService();
}
public async Task<OrderResult> ExecuteAsync(
OrderRequest request)
{
return await _service.ProcessAsync(request);
}
}
The constructor may trigger:
Dependency creation
Configuration loading
Serializer initialization
Client initialization
Reflection
Assembly loading
The actual business operation may be much faster.
A useful benchmark therefore separates:
Initialization
+
Handler execution
=
Total duration
Instrument the Function
Use application-level timing around important operations.
For example:
public async Task<OrderResult> ExecuteAsync(
OrderRequest request,
CancellationToken cancellationToken)
{
var stopwatch = Stopwatch.StartNew();
var result = await _service.ProcessAsync(
request,
cancellationToken);
stopwatch.Stop();
_logger.LogInformation(
"Handler execution completed in {ElapsedMs} ms",
stopwatch.Elapsed.TotalMilliseconds);
return result;
}
This does not measure every platform-level startup operation, but it helps distinguish handler execution from the total observed invocation latency.
Capture Invocation Metadata
Each benchmark record should contain enough information to identify the execution.
For example:
public sealed record BenchmarkResult(
string InvocationId,
bool IsCold,
double DurationMs,
double HandlerDurationMs,
long MemoryBytes,
int RetryCount);
The exact definition can be expanded based on the benchmark requirements.
Cold Starts Are Not Deterministic
One of the biggest benchmarking mistakes is assuming:
Cold Start = Same Latency Every Time
Real workloads vary.
Factors include:
Runtime initialization
Application size
Dependency graph
Memory allocation
Concurrency
Environment availability
Network activity
Workflow scheduling
Therefore, do not rely on one invocation.
Run multiple iterations.
Use Multiple Benchmark Phases
A practical test can have four phases.
Phase 1: Warm Baseline
Run repeated invocations against an already active environment.
Invocation 1
Invocation 2
Invocation 3
...
Invocation N
This establishes the normal execution baseline.
Phase 2: Cold Start
Force new execution environments through controlled conditions.
Measure the resulting latency.
Phase 3: Durable Workflow
Execute a multi-step workflow:
Step A
Step B
Step C
Measure the complete workflow.
Phase 4: Concurrent Workload
Increase concurrency gradually:
1
5
10
25
50
100
The exact levels should match the intended workload.
Example Benchmark Workflow
Consider:
Order Workflow
|
v
Validate Order
|
v
Calculate Price
|
v
Persist Order
|
v
Publish Result
Each activity can run as a separate function execution.
The benchmark should record:
Workflow Start
|
+--> Validation Duration
|
+--> Pricing Duration
|
+--> Persistence Duration
|
+--> Publication Duration
|
v
Workflow End
This allows you to determine whether startup overhead is concentrated in one activity or distributed across the workflow.
A Simple Workflow Measurement Model
Let:
W = Total workflow duration
S = Scheduling overhead
C = Cold-start overhead
E = Activity execution time
P = Persistence overhead
A simplified model is:
W = S + C + E + P
For multiple activities:
W = S + C1 + E1 + P1
+ C2 + E2 + P2
+ C3 + E3 + P3
This is not a platform-level accounting identity.
It is a useful analytical model for identifying where observed latency may originate.
Cold Start Percentage
You can estimate the contribution of startup overhead:
Cold Start Percentage =
Cold Start Time / Total Invocation Time × 100
For example, if:
Total = 800 ms
Startup = 300 ms
then:
Startup contribution = 37.5%
This helps determine whether startup optimization is worth prioritizing.
Why Memory Configuration Matters
Resource configuration can influence startup and execution behavior.
Instead of benchmarking only one configuration:
Configuration A
compare several controlled configurations:
Configuration A
Configuration B
Configuration C
Configuration D
Record:
Latency
Execution time
Memory consumption
Failure rate
Workflow duration
Do not conclude that the smallest resource configuration is automatically the cheapest or fastest.
The benchmark should consider the complete workload.
Compare Cold and Warm Execution
A useful table might look like:
| Scenario | Invocations | Median | P95 | Max |
|---|
| Warm | 100 | Measure | Measure | Measure |
| Cold | 30 | Measure | Measure | Measure |
| Workflow | 30 | Measure | Measure | Measure |
| Concurrent | 30 | Measure | Measure | Measure |
Use your actual measurements rather than fixed values.
Median shows typical behavior.
P95 shows the slower tail.
Maximum can reveal extreme outliers.
Why P95 Matters
Average latency can hide startup problems.
Consider:
90 requests -> 100 ms
10 requests -> 1,000 ms
The average may look acceptable while some users experience much higher latency.
For serverless workloads, tail latency is often more informative.
Measure:
P50
P90
P95
P99
when the workload is large enough to make these percentiles meaningful.
Benchmark Workflow Duration, Not Only Function Duration
Suppose:
Function A = 100 ms
Function B = 100 ms
Function C = 100 ms
It may be tempting to conclude:
Workflow = 300 ms
But a durable workflow can introduce additional overhead:
Function A
|
State persistence
|
Function B
|
State persistence
|
Function C
Therefore:
Workflow Duration > Sum of Activity Durations
can be perfectly normal.
The benchmark should measure both.
Cold Starts Across Multiple Activities
Consider a workflow with five activities:
A -> B -> C -> D -> E
Possible execution behavior:
A = Cold
B = Warm
C = Warm
D = Cold
E = Warm
This means the workflow may experience multiple cold starts.
A benchmark should therefore avoid assuming:
One Workflow = One Cold Start
The number of environments involved can depend on concurrency and execution behavior.
Parallel Activities
A workflow may execute activities in parallel:
+--> Activity A
|
Workflow ----+--> Activity B
|
+--> Activity C
This changes the benchmark.
If all three activities start concurrently, multiple new environments may be created.
The benchmark should record:
Parallelism
Startup latency
Activity duration
Workflow completion time
Sequential vs Parallel Benchmark
Compare:
Sequential
A -> B -> C
with:
Parallel
A
\
+--> B
/
C
The important metric is not simply individual activity latency.
Measure:
End-to-end workflow duration
because that is what the application ultimately experiences.
Test Different Assembly Sizes
.NET applications can contain many dependencies.
A useful experiment compares:
Small Application
|
v
Minimal Dependencies
against:
Large Application
|
v
Many Dependencies
Record the effect on:
Cold initialization
Warm execution
Memory
Deployment package
Workflow latency
This helps identify whether application composition is contributing significantly to startup behavior.
Dependency Initialization
Startup code such as:
builder.Services.AddSingleton<HeavyClient>();
builder.Services.AddSingleton<LargeSerializer>();
builder.Services.AddSingleton<ComplexProcessor>();
may contribute to initialization cost depending on how those dependencies are constructed.
Avoid creating expensive objects unnecessarily during startup.
Prefer lazy initialization when the dependency is not required for every invocation.
For example:
private readonly Lazy<ExpensiveProcessor> _processor;
public Worker()
{
_processor = new Lazy<ExpensiveProcessor>(
CreateProcessor);
}
The optimization should be validated through measurement.
Lazy initialization is not automatically faster if the dependency is needed on every invocation.
Static Initialization
Static initialization can also influence startup.
For example:
private static readonly Dictionary<string, string> Rules =
LoadRules();
If LoadRules() performs expensive work, it can contribute to initialization time.
Measure this rather than assuming.
Avoid Premature Optimization
A common mistake is aggressively rewriting application initialization without first measuring it.
Use this process:
Measure
|
v
Identify Bottleneck
|
v
Change One Variable
|
v
Measure Again
|
v
Compare
This provides a much stronger basis for optimization.
Benchmark Deployment Package Size
Application size can be relevant to startup behavior.
Track:
Application package size
Number of assemblies
Native dependencies
Configuration files
Generated metadata
A smaller deployment package does not guarantee a faster cold start, but it is a useful variable to track.
Trimming and Compilation Strategies
.NET provides deployment and compilation techniques that can affect startup characteristics.
Examples include:
Assembly trimming
Ahead-of-time compilation
Ready-to-run compilation
Source generation
These techniques have trade-offs.
For example, trimming can reduce application footprint but may affect applications that depend heavily on reflection or dynamically discovered types.
Do not apply such techniques blindly.
Benchmark:
Baseline
|
+--> Optimization A
|
+--> Optimization B
|
+--> Optimization C
and compare the results.
JSON Serialization
Agent and workflow systems frequently exchange structured messages.
Serialization can become part of the execution path.
For example:
var payload = JsonSerializer.Serialize(request);
var result =
JsonSerializer.Deserialize<Response>(
payload);
Measure serialization separately when payloads are large.
Source-generated serialization can reduce runtime reflection overhead in suitable applications:
[JsonSerializable(typeof(OrderRequest))]
[JsonSerializable(typeof(OrderResponse))]
internal partial class AppJsonContext
: JsonSerializerContext
{
}
The benefit depends on application characteristics, so benchmark before adopting it as a startup optimization.
Logging Can Affect Benchmarks
Heavy logging can distort performance measurements.
For example:
_logger.LogInformation(
"Complete object: {@Request}",
request);
may serialize large objects.
During benchmarking:
Keep required operational logging.
Avoid logging entire payloads unnecessarily.
Separate benchmark instrumentation from verbose diagnostic logging.
Keep the logging configuration consistent across test runs.
Otherwise, you may end up benchmarking your logging configuration instead of your workflow.
Network Calls During Initialization
A particularly important pattern is performing network calls during startup.
For example:
public Worker()
{
_configuration =
LoadConfigurationFromRemoteService();
}
This can make startup dependent on another service.
The result may be:
Cold Start
|
v
Initialize
|
v
Network Request
|
v
Wait
|
v
Function Ready
Prefer local configuration where appropriate and defer non-essential network operations until they are actually required.
Again, the correct design depends on application requirements.
Database Connections
Do not assume that database connection behavior is identical between cold and warm executions.
A cold environment may need to initialize:
Database client
Connection pool
TLS state
ORM metadata
A warm environment may already have some of these resources available.
Measure:
Startup
Connection acquisition
Query execution
separately where possible.
Entity Framework Core Considerations
For .NET applications using an ORM, model construction can contribute to startup behavior.
For example:
services.AddDbContext<AppDbContext>(
options =>
options.UseSqlServer(connectionString));
The first use of the context may involve additional initialization.
Benchmark:
First query
Second query
Subsequent query
rather than measuring only total function duration.
Durable State Serialization
Durable workflows frequently persist workflow state.
Large state objects can increase:
Serialization time
Storage overhead
Network transfer
Deserialization time
Avoid storing unnecessary data in workflow state.
Instead of:
{
"customer": {
"...": "large object"
},
"documents": [
"... many items ..."
],
"toolResults": [
"... large payload ..."
]
}
store only what is required to resume:
{
"workflowId": "123",
"customerId": "456",
"currentStep": "Validation",
"status": "Running"
}
Keep large payloads outside the workflow state when the architecture allows it.
State Size Is a Benchmark Variable
Include state size in your benchmark.
For example:
Small State
Medium State
Large State
Measure:
Serialization
Persistence
Resume latency
Workflow duration
This can reveal problems that function-level benchmarks miss.
Retry Effects on Benchmark Results
Retries can distort measurements.
Suppose:
Attempt 1 = 500 ms
Attempt 2 = 500 ms
Attempt 3 = 500 ms
The final workflow duration might be:
> 1.5 seconds
If the benchmark counts only successful executions, it can hide reliability problems.
Record:
Total attempts
Successful attempts
Retries
Failed attempts
Workflow completion time
Benchmark With Controlled Failures
A useful experiment deliberately introduces a controlled failure.
For example:
Activity A
|
v
Activity B
|
X
Temporary Failure
|
v
Retry
|
v
Activity B
|
v
Activity C
Measure:
Recovery time
Additional startup cost
Workflow duration
State persistence
This shows how the workflow behaves under realistic failure conditions.
Benchmark Concurrent Workloads
Cold starts often become more visible under concurrency.
Consider:
1 request
versus:
50 concurrent requests
The second scenario may require multiple environments.
Test several levels:
1
5
10
25
50
100
For each level, record:
P50
P95
P99
Throughput
Error rate
Cold-start count
Workflow completion time
Use workload levels that are representative of the intended application.
Warm Reuse Can Distort Results
If you run:
100 invocations
without deliberately creating new environments, many may be warm.
That does not make the benchmark wrong.
It simply means you are measuring warm behavior.
Therefore, clearly label:
Warm Test
and:
Cold Test
Do not mix them into one unexplained number.
A Practical Benchmark Harness
A simple .NET benchmark runner could collect invocation data:
public sealed class InvocationMeasurement
{
public required DateTimeOffset StartedAt { get; init; }
public required TimeSpan Duration { get; init; }
public required bool Success { get; init; }
public required int Attempt { get; init; }
}
Then execute the workflow repeatedly:
var measurements = new List<InvocationMeasurement>();
for (var i = 0; i < iterations; i++)
{
var started = DateTimeOffset.UtcNow;
try
{
await workflow.ExecuteAsync(
cancellationToken);
measurements.Add(
new InvocationMeasurement
{
StartedAt = started,
Duration =
DateTimeOffset.UtcNow - started,
Success = true,
Attempt = 1
});
}
catch
{
measurements.Add(
new InvocationMeasurement
{
StartedAt = started,
Duration =
DateTimeOffset.UtcNow - started,
Success = false,
Attempt = 1
});
}
}
For a real benchmark, add:
Invocation identifiers
Workflow identifiers
Activity timings
Retry counts
Resource configuration
Concurrency level
Application version
Benchmarking Methodology
Use a consistent methodology.
Step 1: Define the Workflow
Example:
Validate
|
Calculate
|
Persist
|
Complete
Step 2: Define Variables
Choose:
Memory configuration
Application package
State size
Concurrency
Payload size
Number of activities
Step 3: Establish Warm Baseline
Run enough invocations to stabilize the measurement.
Step 4: Measure Cold Execution
Use a controlled method for producing new execution environments.
Step 5: Measure Durable Workflow
Record complete workflow duration.
Step 6: Add Concurrency
Repeat the experiment with multiple concurrent executions.
Step 7: Introduce Controlled Failure
Measure retry and recovery behavior.
Step 8: Change One Variable
For example:
Baseline
|
v
Smaller Package
|
v
Measure
Do not change five variables at once.
Example Results Table
Your final benchmark should contain measured data similar to:
| Scenario | Iterations | P50 | P95 | P99 | Failure Rate |
|---|
| Warm function | 100 | Measure | Measure | Measure | Measure |
| Cold function | 30 | Measure | Measure | Measure | Measure |
| Sequential workflow | 30 | Measure | Measure | Measure | Measure |
| Parallel workflow | 30 | Measure | Measure | Measure | Measure |
| Concurrent workload | 30 | Measure | Measure | Measure | Measure |
| Retry scenario | 30 | Measure | Measure | Measure | Measure |
The values should come from your own test environment.
Avoid presenting benchmark numbers from an unrelated environment as universal performance characteristics.
Common Benchmarking Mistakes
Measuring Only Average Latency
Average values can hide cold-start outliers.
Mixing Warm and Cold Invocations
This produces an unclear dataset.
Using Too Few Iterations
A small sample can produce misleading conclusions.
Changing Multiple Variables
You will not know which change caused the result.
Ignoring Workflow Overhead
Function execution time is not the same as workflow duration.
Ignoring Retries
Retries can significantly affect end-to-end latency.
Ignoring State Size
Large workflow state can add serialization and persistence overhead.
Running Only Sequential Tests
Concurrent execution can produce completely different startup behavior.
Optimizing Before Measuring
Startup code should not be rewritten based only on assumptions.
Ignoring Tail Latency
P95 and P99 can be more useful than averages for latency-sensitive workloads.
Troubleshooting
Cold Start Results Are Inconsistent
Check:
Sample size
Concurrency
Environment reuse
Application version
Resource configuration
Network dependencies
Cold-start behavior is naturally variable.
Warm Invocations Are Still Slow
Measure handler execution separately.
The problem may be application logic rather than startup.
Workflow Is Much Slower Than Individual Functions
Inspect:
Scheduling
State persistence
Serialization
Activity transitions
Retries
Network calls
Memory Usage Is Higher During Cold Starts
Check:
Dependency initialization
Static caches
Large object allocation
ORM initialization
Serialization buffers
Concurrent Workloads Produce Large Latency Spikes
Measure:
Cold-start count
Concurrency
P95/P99 latency
Downstream throttling
Connection limits
Retry Scenarios Produce Unexpectedly Long Workflows
Check:
Retry count
Backoff
Timeout
Activity duration
State persistence
Optimization Reduced Package Size but Not Cold Start
The startup bottleneck may be elsewhere.
Measure:
Runtime initialization
Dependency initialization
Network calls
Configuration loading
Handler startup
Do not assume package size is the only variable.
Best Practices
Separate cold and warm benchmarks.
Measure complete durable workflow latency, not only function latency.
Use P50, P95, and P99 rather than average alone.
Run enough iterations to reduce noise.
Record concurrency for every test.
Measure individual workflow activities.
Track retries and failures.
Measure workflow state size.
Avoid unnecessary work during application initialization.
Defer non-essential network operations.
Avoid unnecessary dependency construction during startup.
Keep durable workflow state compact.
Use controlled experiments when comparing configurations.
Change one major variable at a time.
Benchmark both sequential and parallel workflows.
Test realistic concurrent workloads.
Measure resource consumption alongside latency.
Include failure and retry scenarios.
Record application and configuration versions with benchmark results.
Optimize only after identifying a measurable bottleneck.
A Complete Benchmark Architecture
A production-oriented benchmark can be organized like this:
Benchmark Controller
|
+----------------+----------------+
| | |
v v v
Warm Test Cold Test Concurrent Test
| | |
+----------------+----------------+
|
v
Durable Workflow
|
+---------------+---------------+
| | |
v v v
Activity A Activity B Activity C
| | |
+---------------+---------------+
|
v
Measurement Store
|
v
Analysis
|
+---------------+---------------+
| | |
v v v
P50/P95 Errors Resource Use
This structure separates workload generation from measurement and analysis.
What a Good Benchmark Should Tell You
At the end of the experiment, you should be able to answer:
How much slower is a cold invocation than a warm invocation?
How much of the latency comes from initialization?
How much time does durable state management add?
How does concurrency affect startup behavior?
How frequently do workflows encounter cold environments?
How do retries affect end-to-end duration?
Does application initialization dominate startup?
Does workflow state size affect execution time?
Which configuration provides the best balance between
latency and resource consumption?
If the benchmark cannot answer these questions, it probably needs better instrumentation.
Conclusion
Cold starts are an important consideration when building .NET workloads on Lambda, and durable workflows make the performance model more complex.
A function may be fast once initialized but still contribute significant latency when a new execution environment is created.
A durable workflow can multiply this effect across multiple activities:
Workflow
|
+--> Activity A
| |
| Cold Start
|
+--> Activity B
| |
| Warm Start
|
+--> Activity C
|
Cold Start
This is why a useful benchmark must go beyond measuring one function invocation.
Measure:
Cold Latency
Warm Latency
Workflow Duration
Activity Duration
Concurrency
Retries
State Size
Memory
Tail Latency
Then change one variable at a time and compare the results.
The most important lesson is simple:
Do not optimize cold starts based on assumptions. Measure the complete workflow, identify where the latency comes from, and optimize the actual bottleneck.
For durable .NET workloads, the best performance decisions come from understanding the relationship between runtime initialization, application startup, workflow orchestration, state persistence, concurrency, and recovery behavior.