LLMs  

Benchmarking .NET Lambda Cold Starts with Durable Workflows

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.

MetricPurpose
Cold-start latencyMeasures initialization overhead
Warm latencyEstablishes execution baseline
Workflow durationMeasures end-to-end behavior
Initialization timeIdentifies startup cost
Handler execution timeSeparates application work
Memory usageHelps evaluate resource configuration
Retry countShows reliability behavior
Activity latencyIdentifies slow workflow steps
Environment reuseHelps understand warm execution
Concurrent execution latencyMeasures 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:

ScenarioInvocationsMedianP95Max
Warm100MeasureMeasureMeasure
Cold30MeasureMeasureMeasure
Workflow30MeasureMeasureMeasure
Concurrent30MeasureMeasureMeasure

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:

ScenarioIterationsP50P95P99Failure Rate
Warm function100MeasureMeasureMeasureMeasure
Cold function30MeasureMeasureMeasureMeasure
Sequential workflow30MeasureMeasureMeasureMeasure
Parallel workflow30MeasureMeasureMeasureMeasure
Concurrent workload30MeasureMeasureMeasureMeasure
Retry scenario30MeasureMeasureMeasureMeasure

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

  1. Separate cold and warm benchmarks.

  2. Measure complete durable workflow latency, not only function latency.

  3. Use P50, P95, and P99 rather than average alone.

  4. Run enough iterations to reduce noise.

  5. Record concurrency for every test.

  6. Measure individual workflow activities.

  7. Track retries and failures.

  8. Measure workflow state size.

  9. Avoid unnecessary work during application initialization.

  10. Defer non-essential network operations.

  11. Avoid unnecessary dependency construction during startup.

  12. Keep durable workflow state compact.

  13. Use controlled experiments when comparing configurations.

  14. Change one major variable at a time.

  15. Benchmark both sequential and parallel workflows.

  16. Test realistic concurrent workloads.

  17. Measure resource consumption alongside latency.

  18. Include failure and retry scenarios.

  19. Record application and configuration versions with benchmark results.

  20. 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.