Azure Functions Cold Start: Optimization Without Premium
Azure Functions are powerful, but cold starts can turn a snappy API into a frustrating experience. After optimizing Functions across several production projects, here are the strategies that actually move the needle — without reaching for the Premium plan.
Measuring Cold Start
Before optimizing, measure. Cold start time varies by runtime, dependencies, and region. Use Application Insights to track it:
requests
| where timestamp > ago(24h)
| extend coldStart = tobool(customDimensions["_MS_ProcessedByMetricExtraction"])
| summarize
avg(duration),
percentile(duration, 95),
count()
by bin(timestamp, 1h), coldStartIn my experience, .NET 8 isolated worker Functions cold-start in 2-4 seconds. With optimization, you can get that under 1.5 seconds on the Consumption plan.
Strategy 1: Trim Your Dependencies
Every NuGet package adds to startup time. Audit your dependencies ruthlessly:
<!-- before: 45 packages, 8-second cold start -->
<PackageReference Include="MediatR" Version="12.0.0" />
<PackageReference Include="AutoMapper" Version="13.0.0" />
<PackageReference Include="FluentValidation" Version="11.0.0" />
<PackageReference Include="Serilog.Sinks.Everything" Version="*" />
<!-- after: 20 packages, 3-second cold start -->
<!-- removed AutoMapper: manual mapping for 4 DTOs is fine -->
<!-- removed FluentValidation: data annotations for simple validation -->
<!-- trimmed Serilog to just the sinks we use -->The biggest wins come from removing packages that pull in large dependency trees. `AutoMapper` and `FluentValidation` are common offenders in small Functions that don't need them.
Strategy 2: Lazy Initialization
Not every HTTP trigger needs every service. Initialize expensive resources lazily:
public class OrderFunction
{
private readonly Lazy<ServiceBusClient> _serviceBus;
private readonly IOrderRepository _orders;
public OrderFunction(
IOrderRepository orders,
IConfiguration config)
{
_orders = orders;
_serviceBus = new Lazy<ServiceBusClient>(() =>
new ServiceBusClient(
config["ServiceBusConnection"]));
}
[Function("GetOrder")]
public async Task<IActionResult> GetOrder(
[HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req,
string id)
{
// ServiceBusClient not initialized for read-only operations
return new OkObjectResult(await _orders.GetByIdAsync(id));
}
}Strategy 3: ReadyToRun Compilation
Publish with ReadyToRun to pre-compile assemblies to native code:
<PropertyGroup>
<PublishReadyToRun>true</PublishReadyToRun>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>This increases the deployment package size but significantly reduces JIT compilation time during cold start. In my tests, this alone saves 300-500ms.
Strategy 4: The Timer Trigger Keep-Warm
The simplest approach: a timer trigger that runs every few minutes to keep the instance warm:
[Function("KeepWarm")]
public void KeepWarm(
[TimerTrigger("0 */4 * * * *")] TimerInfo timer,
FunctionContext context)
{
context.GetLogger("KeepWarm")
.LogInformation("Keep-warm ping at {Time}", DateTime.UtcNow);
}This isn't free — you pay for the executions — but at ~6 cents per million executions, it's dramatically cheaper than the Premium plan. It doesn't guarantee zero cold starts (scale-out still causes them), but it eliminates the most common case.
Strategy 5: Minimal Startup Configuration
The isolated worker model's `Program.cs` runs on every cold start. Keep it lean:
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureServices(services =>
{
// register only what's needed
services.AddSingleton<IOrderRepository, OrderRepository>();
services.AddHttpClient<IExternalApi, ExternalApiClient>();
})
.Build();
host.Run();Avoid scanning assemblies for auto-registration, loading configuration from remote sources during startup, or initializing database connections eagerly.
When to Just Use Premium
If your P95 cold start exceeds your SLA requirements and the above strategies aren't enough, the Premium plan with pre-warmed instances is the right call. The cost difference is significant ($150+/month vs cents), but some workloads justify it — particularly user-facing APIs where latency directly impacts experience.
Key Takeaways
- Measure cold start with Application Insights before optimizing
- Trim dependencies — fewer packages means faster startup
- Use lazy initialization for services not needed by every trigger
- Enable ReadyToRun compilation for 300-500ms savings
- A timer keep-warm is cheap insurance against the most common cold starts
Share this article
About the Author
Georg is a senior solution architect specializing in .NET, Azure, and Dynamics 365. He helps organizations design and build scalable, maintainable enterprise systems. When he's not writing code, he's writing about it here.
Learn more about Georg