Cold Starts in Azure Functions: What You Can Actually Do About Them

Cold starts are the Achilles heel of serverless. Practical techniques to drastically reduce Azure Functions startup time.

Jean-Pierre Broeders

Freelance DevOps Engineer

March 13, 20265 min. read

Cold Starts in Azure Functions: What You Can Actually Do About Them

Serverless is great until the first request of the day hits. That's when it shows up: two, three, sometimes five seconds of wait time. The cold start. For a background job, nobody cares. But when there's a user staring at a loading spinner? That's a problem.

Why cold starts happen

Azure Functions doesn't run permanently. On the Consumption plan, the underlying infrastructure gets deallocated after a few minutes of inactivity. When a new request arrives, Azure needs to spin up a host, load the runtime, resolve dependencies, and only then execute the code. That takes time.

Duration depends on several factors. The runtime matters — .NET functions generally start faster than Node.js or Python because the .NET worker sits closer to Azure's native infrastructure. But dependency count plays a role too. A function with twenty NuGet packages needs more startup time than one with two.

Measure before optimizing

Before changing anything: measure. Application Insights provides cold start metrics by default, but most teams never look at them. This Kusto query pulls average startup time:

requests
| where timestamp > ago(7d)
| where name == "FunctionName"
| extend coldStart = tobool(customDimensions["ms-azurefunctions-coldstart"])
| where coldStart == true
| summarize avg(duration), percentile(duration, 95), count() by bin(timestamp, 1h)

The P95 value is what matters. Averages lie — if 5% of requests take eight seconds, that's a real problem hidden behind a 400ms average.

Premium Plan: the obvious fix

The Premium plan (Elastic Premium) keeps at least one warm instance available at all times. No more cold starts for the first request. Additional instances still scale on-demand, but that baseline is always there.

Costs are higher. Expect around €150-200 per month for an EP1 instance, compared to maybe €10-20 on Consumption for an average workload. Whether that's worth it depends on the use case.

PlanCold StartCost (indicative)Best for
Consumption2-10 seconds€5-30/monthBackground tasks, low volume
Premium (EP1)None (warm instance)€150-200/monthAPIs, user-facing endpoints
Dedicated (App Service)None€50-300/monthPredictable, constant load

Keep-warm strategy with a timer trigger

For those not ready to move to Premium, a simple timer trigger that hits the function every four minutes keeps the host alive:

[Function("KeepWarm")]
public async Task Run(
    [TimerTrigger("0 */4 * * * *")] TimerInfo timer,
    FunctionContext context)
{
    var logger = context.GetLogger("KeepWarm");
    logger.LogInformation("Warm ping at {time}", DateTime.UtcNow);
}

Simple and effective. But it's a workaround, not a solution. With multiple functions in the same function app, only one keep-warm trigger is needed — the entire host stays active. The downside: paying for those extra executions. On Consumption the cost is negligible, but it doesn't feel clean.

Minimize dependencies

Every dependency that needs loading costs startup time. A few concrete measures:

Lazy loading — initialize heavy services only on first use:

private static readonly Lazy<HttpClient> _httpClient = 
    new(() => new HttpClient());

private static readonly Lazy<CosmosClient> _cosmosClient = 
    new(() => new CosmosClient(
        Environment.GetEnvironmentVariable("CosmosConnection")));

Trim unused packages — after a few months of development, there are always NuGet packages nobody needs anymore. Running dotnet list package followed by cleanup can save hundreds of milliseconds.

Ready-to-run publishing — compile to native images with ReadyToRun:

<PropertyGroup>
    <PublishReadyToRun>true</PublishReadyToRun>
</PropertyGroup>

This increases the deployment package size, but JIT compilation at startup gets skipped. For .NET isolated worker functions, the difference is noticeable.

Isolated worker vs In-process

Since .NET 8, the isolated worker model is the default. It runs in a separate process, providing more flexibility but also slightly more overhead at startup. Communication between host and worker goes over gRPC.

In-process functions (the old model) start faster because they run in the same process as the host. But Microsoft is phasing this out — .NET 9 only supports isolated. So optimizing within the isolated model is the only sustainable strategy.

One thing that helps: minimize middleware components. Each middleware in the pipeline gets initialized at startup. Two or three is fine. Ten is too many.

Network and region

Does the function call a database or external API? Then network latency counts toward the first request. A three-second cold start plus a two-second database connection feels like a five-second cold start, even though half of it is network.

Connection pooling helps. Static HttpClient instances — never create them per-request. And deploy the function app in the same region as the database. Sounds obvious, but it gets forgotten regularly in multi-region setups.

The pragmatic approach

Not every function needs optimization. A nightly batch job running at 3 AM? Nobody notices a five-second cold start there. An API endpoint behind a mobile app? That's a different story.

The strategy that works best in practice:

  1. Measure cold start times via Application Insights
  2. Identify which functions are user-facing
  3. Premium plan for critical endpoints, Consumption for the rest
  4. Optimize dependencies regardless of plan
  5. Monitor after changes — improvements aren't always as large as expected

Cold starts aren't solvable, but they are manageable. The difference between a frustrating and an acceptable experience often comes down to a few targeted adjustments.

Want to stay updated?

Subscribe to my newsletter or get in touch for freelance projects.

Get in Touch