Level: Intermediate to Advanced Technologies: Azure Functions v4 / C# / .NET 8 / Application Insights / Azure Monitor
About this guide: This document is a complete reference on optimizing Azure Functions for performance and cost. It covers in depth hosting plans, cold start anatomy, architectural best practices, hardening, scaling configuration, Durable Functions for long-running workflows, and cost monitoring.
Table of Contents
- Hosting Plans – Comparison and Optimal Choice
- Cold Start – Anatomy and Solutions
- Architecture and Code Best Practices
- Hardening and Resilience
- Scaling Configuration
- Durable Functions for Long-Running Workloads
- Monitoring and Cost Analysis
- Advanced Optimization
- Performance Checklist
- Glossary
Module 1 – Hosting Plans: Comparison and Optimal Choice {#module-1}
Understanding Azure Functions’ PaaS Context
Azure Functions fits within Azure’s Platform as a Service (PaaS) ecosystem. Unlike VMs (IaaS), with PaaS you don’t manage VMs, the OS, or runtimes — you focus on code.
flowchart TD
subgraph "Azure PaaS Ecosystem"
AKS[Azure Kubernetes Service\nOrchestrated containers]
AppService[Azure App Service\nWeb apps and APIs]
LogicApps[Azure Logic Apps\nLow-code workflows]
Functions[Azure Functions\nEvent-driven pro-code]
ContainerApps[Azure Container Apps\nServerless containers]
end
subgraph "Serverless (Scale to Zero)"
Functions
ContainerApps
LogicApps
end
subgraph "Non-Serverless (Scale from Min)"
AKS
AppService
end
style Functions fill:#68217A,color:#fff
The 5 Hosting Plans in Detail
Consumption Plan: True Serverless
Advantages:
✅ Pay only for actual executions
✅ Auto-scale from 0 to 200 instances
✅ 1 million free executions/month
✅ 400,000 GB-seconds free/month
✅ No scaling configuration needed
Disadvantages:
❌ Cold start unavoidable (1–10+ seconds)
❌ Maximum timeout: 10 minutes
❌ No native VNet integration
❌ Unpredictable performance during spikes
❌ Limited memory: 1.5 GB
Flex Consumption Plan: The Best of Both Worlds
Advantages:
✅ Pay-per-use like Consumption
✅ VNet Integration available
✅ Configurable "always ready" instances (reduce cold start)
✅ Configurable memory: 512 MB to 4 GB
✅ Timeout up to 60 minutes
✅ Better scaling options
Disadvantages:
❌ Cost of always-ready instances
❌ Newer = less mature documentation
Premium (Elastic Premium) Plan
Advantages:
✅ NO cold start (pre-warmed instances)
✅ Unlimited timeout
✅ Full VNet Integration
✅ Private Endpoints
✅ Memory 3.5 GB to 14 GB
✅ Customizable scaling
Disadvantages:
❌ Minimum cost even without traffic (~$150-600/month)
❌ Overkill for small workloads
Dedicated (App Service Plan)
Advantages:
✅ Reuses existing App Service infrastructure
✅ No cold start
✅ Unlimited timeout
✅ Same SKU as your existing web apps
Disadvantages:
❌ Pay even when idle
❌ Risk of "noisy neighbor" if shared
❌ Less automatic scaling
Ultimate Comparison Table
| Feature | Consumption | Flex Consumption | Premium EP1 | Premium EP2 | Dedicated B2 | Container Apps |
|---|---|---|---|---|---|---|
| Billing model | Pay-per-exec | Pay-per-exec + always-ready | Hourly | Hourly | Monthly | vCPU/Memory |
| Min cost/month | $0 | $0+ | ~$150 | ~$300 | ~$30 | $0+ |
| Cold Start | Yes (1-10s) | Configurable | No | No | No | Variable |
| Default timeout | 5 min | 30 min | 30 min | 30 min | 30 min | Unlimited |
| Maximum timeout | 10 min | 60 min | Unlimited | Unlimited | Unlimited | Unlimited |
| VNet Integration | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Private Endpoints | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
| vCPU per instance | Shared | 1 | 1 | 2 | 2 | Configurable |
| Memory per instance | 1.5 GB | 0.5-4 GB | 3.5 GB | 7 GB | 3.5 GB | Configurable |
| Scale to Zero | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
| Always Ready | ❌ | ✅ (paid) | ✅ (included) | ✅ (included) | ✅ | ✅ |
| Max Scale Out | 200 | Flexible | 100 | 100 | Per SKU | Unlimited |
Decision Tree for Choosing a Plan
flowchart TD
A{What is your profile?} --> B{Very unpredictable traffic\nor sporadic loads?}
B -->|Yes + no VNet| C[🟢 Consumption Plan\nMinimal cost, scale to zero]
B -->|Yes + need VNet| D[🔵 Flex Consumption\nServerless + networking]
B -->|No| E{Constant\nor predictable load?}
E -->|Yes + critical performance| F{Budget available?}
F -->|Available| G[🔴 Premium EP1/EP2\nGuaranteed performance]
F -->|Limited| H{Existing App Service?}
H -->|Yes| I[🟣 Dedicated Plan\nReuse infra]
H -->|No| J[🔵 Flex Consumption\nGood compromise]
E -->|No / Kubernetes| K[⚫ Container Apps\nMaximum flexibility]
style C fill:#107C10,color:#fff
style D fill:#0078D4,color:#fff
style G fill:#D83B01,color:#fff
Module 2 – Cold Start: Anatomy and Solutions {#module-2}
What is a Cold Start?
When your function is triggered and there is no available instance, Azure must create a new one. This process takes time — that’s the cold start.
sequenceDiagram
participant Event as Trigger Event
participant Azure as Azure Platform
participant Host as Function Host
participant Runtime as .NET Runtime
participant Code as Your Code
Event->>Azure: Trigger (e.g., HTTP request)
Note over Azure,Code: Cold Start (if no instance available)
Azure->>Azure: Find an available physical server (2-3s)
Azure->>Host: Allocate and start the Function Host (1-2s)
Host->>Runtime: Load the .NET runtime (0.5-2s)
Runtime->>Code: Load your NuGet dependencies (0.5-3s)
Code-->>Event: Final response (Total: 2-10+ seconds!)
Note over Azure,Code: Warm Start (instance already available)
Event->>Code: Execute directly (50-200ms)
Factors Influencing Cold Start Duration
| Factor | Impact | Optimization |
|---|---|---|
| Number of NuGet packages | +++ | Reduce unnecessary dependencies |
| Deployed package size | ++ | Run-From-Package, avoid large ZIP deployment |
| .NET runtime | + | .NET 8 is faster than .NET 6 |
| Hosting plan | +++ | Premium eliminates cold starts |
| Process isolation | + | Isolated Worker slightly slower than in-process |
| Azure region | + | Choose a region close to users |
| Number of DI dependencies | ++ | Avoid slow initializations at startup |
Measuring Cold Starts
// KQL in Application Insights - Identify cold starts
requests
| where timestamp > ago(24h)
| where name contains "yourFunctionName"
| extend isColdStart = customDimensions["ColdStart"] == "True"
| summarize
TotalRequests = count(),
ColdStarts = countif(isColdStart == true),
ColdStartRate = round(countif(isColdStart == true) * 100.0 / count(), 1),
AvgDuration_Cold = avgif(duration, isColdStart == true),
AvgDuration_Warm = avgif(duration, isColdStart == false)
| project TotalRequests, ColdStarts, ColdStartRate, AvgDuration_Cold, AvgDuration_Warm
Solutions to Reduce/Eliminate Cold Starts
Solution 1: Minimize Initialization Time
// Program.cs - Optimizing initialization
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureServices(services =>
{
// ✅ Use AddDbContextPool instead of AddDbContext
// Pool reuses DB connections
services.AddDbContextPool<AppDbContext>(options =>
options.UseSqlServer(
Environment.GetEnvironmentVariable("SqlConnection"),
b => b.EnableRetryOnFailure(3)));
// ✅ Singleton for expensive-to-initialize clients
services.AddSingleton<CosmosClient>(sp =>
{
var options = new CosmosClientOptions
{
SerializerOptions = new CosmosSerializationOptions
{
PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
}
};
return new CosmosClient(
Environment.GetEnvironmentVariable("CosmosDBConnection"),
options);
});
// ✅ HttpClient via factory (singleton pool)
services.AddHttpClient("InventoryAPI", client =>
{
client.BaseAddress = new Uri(Environment.GetEnvironmentVariable("InventoryApiUrl")!);
client.Timeout = TimeSpan.FromSeconds(30);
});
// ❌ AVOID: slow initialization in ConfigureServices
// services.AddSingleton<ISlow>(new SlowInitializationService()); // Slows ALL cold starts
})
.Build();
Solution 2: Reduce Package Size
# Check published package size
dotnet publish --configuration Release --output ./publish-check
du -sh ./publish-check # Show size
# Identify large dependencies
find ./publish-check -name "*.dll" | xargs ls -la | sort -k5 -rn | head -20
<!-- .csproj - Publication optimization -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<!-- Enable trimming (removes unused code) -->
<PublishTrimmed>true</PublishTrimmed>
<!-- Further reduce with AOT compilation if compatible -->
<SelfContained>false</SelfContained>
<!-- Disable XML generation (saves space) -->
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>
</Project>
Solution 3: Always-Ready Instances (Flex Consumption)
# Configure always-ready instances to reduce cold starts
az functionapp config set \
--name func-myapp-prod \
--resource-group rg-myapp \
--settings "functionsAlwaysReadyConfig=[{\"name\":\"*\",\"instanceCount\":2}]"
Solution 4: Switch to Premium Plan
# Migrate from Consumption to Premium (zero cold start)
az functionapp plan create \
--name asp-myapp-premium \
--resource-group rg-myapp \
--sku EP1 \
--is-linux false \
--min-instances 1 # At least 1 pre-warmed instance
az functionapp update \
--name func-myapp-prod \
--resource-group rg-myapp \
--plan asp-myapp-premium
Module 3 – Architecture and Code Best Practices {#module-3}
Core Principle: Small and Focused Functions
“Keep your functions small and single-purpose”
Why?
flowchart LR
subgraph "❌ Bad Architecture"
Big[1 large function\n500 lines\nDoes everything\n1000ms execution]
Big --> DB[DB]
Big --> Email[Email]
Big --> Storage[Storage]
Big --> API[External API]
end
subgraph "✅ Good Architecture"
HTTP[HTTP Function\n50ms\nValidate + Route] -->|Queue| Q[Azure Queue]
Q --> DB_F[DB Function\n100ms\nSave]
Q --> Email_F[Email Function\n200ms\nNotify]
DB_F --> Storage_F[Storage Function\n150ms\nArchive]
end
style Big fill:#D83B01,color:#fff
style HTTP fill:#107C10,color:#fff
Execution Lifecycle and Instance Pools
stateDiagram-v2
[*] --> WarmPool: Instance available in pool
WarmPool --> Running: Invocation received
Running --> WarmPool: Execution complete
Running --> Timeout: Duration > configured timeout
Timeout --> [*]: Instance terminated
[*] --> ColdStart: No instance available
ColdStart --> Allocating: Find a server
Allocating --> Starting: Start the runtime
Starting --> Loading: Load dependencies
Loading --> Running: Code ready to execute
Code Best Practices for Performance
Reusing Expensive Resources
using Azure.Storage.Blobs;
using Microsoft.Azure.Cosmos;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace OrderService.Optimized;
/// <summary>
/// ✅ CORRECT PATTERN: Clients reused via DI (Singleton)
/// HTTP, Cosmos DB, Storage clients are created ONCE
/// and reused for all invocations
/// </summary>
public class OptimizedFunction
{
// All these dependencies are Singletons - created only once
private readonly ILogger<OptimizedFunction> _logger;
private readonly CosmosClient _cosmosClient;
private readonly BlobServiceClient _blobClient;
private readonly HttpClient _httpClient;
public OptimizedFunction(
ILogger<OptimizedFunction> logger,
CosmosClient cosmosClient, // Singleton ✅
BlobServiceClient blobClient, // Singleton ✅
IHttpClientFactory httpClientFactory) // Singleton Factory ✅
{
_logger = logger;
_cosmosClient = cosmosClient;
_blobClient = blobClient;
_httpClient = httpClientFactory.CreateClient("default");
}
[Function("ProcessWithOptimizedResources")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
{
// Use pre-initialized clients (no internal cold start)
var container = _cosmosClient.GetContainer("OrdersDB", "Items");
var order = await req.ReadFromJsonAsync<OrderRequest>();
await container.CreateItemAsync(order, new PartitionKey(order!.OrderId.ToString()));
_logger.LogInformation("Order {OrderId} created", order.OrderId);
var response = req.CreateResponse(System.Net.HttpStatusCode.Created);
return response;
}
}
Optimization with JSON Source Generators (C# 11+)
// Pre-compile JSON serialization for better performance
using System.Text.Json.Serialization;
// Declare the serialization context
[JsonSerializable(typeof(OrderRequest))]
[JsonSerializable(typeof(OrderResponse))]
[JsonSerializable(typeof(List<OrderRequest>))]
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
internal partial class AppJsonContext : JsonSerializerContext { }
// Program.cs - Register the context
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults(builder =>
{
builder.UseNewtonsoftJson(); // Or System.Text.Json
})
.ConfigureServices(services =>
{
services.Configure<JsonSerializerOptions>(options =>
{
// Use pre-compiled context = 2-3x faster!
options.TypeInfoResolver = AppJsonContext.Default;
});
})
.Build();
Async/Await: Golden Rules
// ❌ Bad: Blocks thread with .Result or .Wait()
public HttpResponseData BadAsyncExample(HttpRequestData req)
{
var data = GetDataAsync().Result; // ❌ BLOCKS! Potential deadlock!
var more = GetMoreDataAsync().GetAwaiter().GetResult(); // ❌ ALSO BAD!
return req.CreateResponse(System.Net.HttpStatusCode.OK);
}
// ❌ Bad: Fire and forget without error handling
public async Task<HttpResponseData> BadFireAndForget(HttpRequestData req)
{
_ = DoWorkAsync(); // ❌ Exception silently lost!
return req.CreateResponse(System.Net.HttpStatusCode.OK);
}
// ✅ Correct: Properly await
public async Task<HttpResponseData> GoodAsyncExample(HttpRequestData req)
{
var data = await GetDataAsync(); // ✅ Non-blocking
var more = await GetMoreDataAsync(); // ✅ Non-blocking
return req.CreateResponse(System.Net.HttpStatusCode.OK);
}
// ✅ Correct: Controlled parallelism
public async Task<HttpResponseData> GoodParallelExample(HttpRequestData req)
{
// Launch in parallel when operations are independent
var task1 = GetDataAsync();
var task2 = GetMoreDataAsync();
await Task.WhenAll(task1, task2); // ✅ Parallel and non-blocking!
var result = new { data = task1.Result, more = task2.Result };
var response = req.CreateResponse(System.Net.HttpStatusCode.OK);
await response.WriteAsJsonAsync(result);
return response;
}
Avoiding Long-Running Functions
// ❌ PROBLEM: Processing 10,000 records in a single function
// → Timeout on Consumption Plan (10 min max)
// → Consumes a lot of memory
[Function("BadBulkProcessor")]
public async Task BadBulkProcessor(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
{
var allRecords = await GetAll10000RecordsAsync(); // 10,000 records
foreach (var record in allRecords) // Could take 15+ minutes!
{
await ProcessRecord(record);
}
}
// ✅ SOLUTION: Split into small batches via Queue
[Function("SmartBulkInitiator")]
public async Task<HttpResponseData> SmartBulkInitiator(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
{
var recordIds = await GetAll10000RecordIdsAsync(); // Get only IDs
// Send each ID to a queue for async processing
var queueClient = new QueueClient(
Environment.GetEnvironmentVariable("AzureWebJobsStorage"),
"bulk-process-queue");
// Send in batches of 100 to optimize API calls
foreach (var batch in recordIds.Chunk(100))
{
await queueClient.SendMessageAsync(
System.Text.Json.JsonSerializer.Serialize(batch));
}
var response = req.CreateResponse(System.Net.HttpStatusCode.Accepted);
await response.WriteStringAsync($"{recordIds.Count} records queued");
return response;
}
// Each batch is processed by a separate function (fast and scalable)
[Function("ProcessBatch")]
public async Task ProcessBatch(
[QueueTrigger("bulk-process-queue")] Guid[] recordIds)
{
// Process only 100 records (fast!)
foreach (var id in recordIds)
{
await ProcessRecord(id);
}
}
Module 4 – Hardening and Resilience {#module-4}
Designing for Resilience
flowchart TD
Invoke[Function Invocation] --> Retry{Retry Logic}
Retry -->|Success| Success[✅ OK Result]
Retry -->|Transient failure| Wait[Wait\nExponential Backoff]
Wait --> Retry
Retry -->|Permanent failure| DeadLetter[📬 Dead Letter Queue]
DeadLetter --> Alert[🔔 Ops team alert]
Alert --> Manual[👤 Manual review]
Invoke -->|Concurrent invocations| Lock{Shared resource?}
Lock -->|Yes| Mutex[Use lock/semaphore]
Lock -->|No| Parallel[Parallel processing]
style Success fill:#107C10,color:#fff
style DeadLetter fill:#D83B01,color:#fff
Retry with Polly
using Microsoft.Extensions.DependencyInjection;
using Polly;
using Polly.Extensions.Http;
// Program.cs - Configure retry policies with Polly
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureServices(services =>
{
// Retry policy for external APIs
var retryPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => (int)msg.StatusCode >= 500)
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)) // 2s, 4s, 8s
+ TimeSpan.FromMilliseconds(Random.Shared.Next(0, 500)), // Jitter
onRetry: (outcome, timespan, retryAttempt, context) =>
{
Console.WriteLine($"Retry {retryAttempt} after {timespan.TotalSeconds}s. Reason: {outcome.Exception?.Message}");
});
// Circuit Breaker: cuts the service after 5 consecutive failures
var circuitBreakerPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromSeconds(30),
onBreak: (outcome, duration) => Console.WriteLine($"Circuit breaker OPEN for {duration.TotalSeconds}s"),
onReset: () => Console.WriteLine("Circuit breaker CLOSED - Service restored"),
onHalfOpen: () => Console.WriteLine("Circuit breaker testing..."));
// Combine both policies
var combinedPolicy = Policy.WrapAsync(retryPolicy, circuitBreakerPolicy);
services.AddHttpClient("ExternalPaymentAPI", client =>
{
client.BaseAddress = new Uri("https://api.payment.com/");
client.Timeout = TimeSpan.FromSeconds(30);
})
.AddPolicyHandler(combinedPolicy);
})
.Build();
Idempotence: Key to Resilience
/// <summary>
/// Idempotent Queue function: can be called multiple times with the same message
/// without different effect (Azure Queue commands can be delivered multiple times)
/// </summary>
public class IdempotentOrderProcessor
{
private readonly ILogger<IdempotentOrderProcessor> _logger;
private readonly AppDbContext _dbContext;
public IdempotentOrderProcessor(
ILogger<IdempotentOrderProcessor> logger,
AppDbContext dbContext)
{
_logger = logger;
_dbContext = dbContext;
}
[Function("ProcessOrderIdempotent")]
public async Task Run(
[QueueTrigger("orders-queue", Connection = "AzureWebJobsStorage")]
OrderQueueMessage message,
FunctionContext context)
{
_logger.LogInformation(
"Processing order {OrderId} (dequeue count: {Count})",
message.OrderId,
message.DequeueCount);
// 1. Check if already processed (IDEMPOTENCE)
var existing = await _dbContext.ProcessedOrders
.AsNoTracking()
.FirstOrDefaultAsync(o => o.OrderId == message.OrderId);
if (existing is not null)
{
_logger.LogInformation(
"Order {OrderId} already processed on {ProcessedAt}. Skipped.",
message.OrderId,
existing.ProcessedAt);
return; // Idempotent exit
}
// 2. Processing with atomic transaction
await using var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
// Mark as "processing" first
_dbContext.ProcessedOrders.Add(new ProcessedOrder
{
OrderId = message.OrderId,
ProcessedAt = DateTime.UtcNow,
Status = "Processing"
});
await _dbContext.SaveChangesAsync();
// Business processing
await ProcessOrderBusinessLogic(message);
// Update final status
await _dbContext.ProcessedOrders
.Where(o => o.OrderId == message.OrderId)
.ExecuteUpdateAsync(s => s
.SetProperty(o => o.Status, "Completed")
.SetProperty(o => o.CompletedAt, DateTime.UtcNow));
await transaction.CommitAsync();
_logger.LogInformation("Order {OrderId} processed successfully", message.OrderId);
}
catch
{
await transaction.RollbackAsync();
throw; // Allows automatic retry
}
}
private async Task ProcessOrderBusinessLogic(OrderQueueMessage message)
{
// Business logic here
await Task.Delay(100); // Simulated
}
}
Thread Safety and Avoiding Race Conditions
// ❌ DANGEROUS PATTERN: potential race condition with concurrent resource access
private static int _counter = 0;
[Function("DangerousCounter")]
public async Task DangerousFunction(
[QueueTrigger("work-queue")] WorkItem item)
{
// ❌ Race condition: multiple simultaneous invocations can corrupt _counter
_counter++; // NOT THREAD-SAFE!
await SaveCounterAsync(_counter);
}
// ✅ CORRECT: Use Interlocked for simple atomic operations
private static int _safeCounter = 0;
[Function("SafeCounter")]
public async Task SafeFunction(
[QueueTrigger("work-queue")] WorkItem item)
{
// ✅ Thread-safe: atomic operation
var newCount = Interlocked.Increment(ref _safeCounter);
await SaveCounterAsync(newCount);
}
// ✅ EVEN BETTER: Use stateless services
// Store state in Azure Storage (not in memory)
[Function("StatelessCounter")]
public async Task StatelessCounterFunction(
[QueueTrigger("work-queue")] WorkItem item,
[TableInput("counters", "main", "total", Connection = "AzureWebJobsStorage")]
CounterEntity? counter)
{
// State is in Azure Table Storage, not in memory
// No concurrency issue between instances
}
Module 5 – Scaling Configuration {#module-5}
Understanding Azure Functions Scaling
flowchart LR
subgraph "Consumption Plan - Dynamic Scaling"
Low[Queue: 0 messages] -->|No invocations| Zero[0 active instances\nCost: $0]
Medium[Queue: 50 messages] -->|Moderate invocations| Some[3-5 instances\nCost: per exec]
High[Queue: 10,000 messages] -->|Spike!| Many[50-100 instances\nCost: per exec]
Many -->|Queue empty| Zero2[Back to 0 instances\nCost: $0]
end
Scaling Configuration in host.json
{
"version": "2.0",
"functionTimeout": "00:10:00",
"extensions": {
"queues": {
"batchSize": 16,
"newBatchThreshold": 8,
"maxDequeueCount": 5,
"visibilityTimeout": "00:00:30",
"maxPollingInterval": "00:00:02"
},
"serviceBus": {
"maxConcurrentCalls": 16,
"maxConcurrentSessions": 8,
"prefetchCount": 0,
"autoCompleteMessages": true,
"sessionIdleTimeout": "00:01:00",
"maxAutoLockRenewalDuration": "00:05:00"
},
"eventHubs": {
"batchCheckpointFrequency": 5,
"partitionReceiverOptions": {
"ownerLevel": 0
},
"eventProcessorOptions": {
"maxBatchSize": 100,
"prefetchCount": 300,
"loadBalancingUpdateInterval": "00:00:10",
"partitionOwnershipExpirationInterval": "00:01:00"
}
}
},
"concurrency": {
"dynamicConcurrencyEnabled": true,
"snapshotPersistenceEnabled": true
},
"healthMonitor": {
"enabled": true,
"healthCheckInterval": "00:00:10",
"healthCheckWindow": "00:02:00",
"healthCheckThreshold": 6,
"counterThreshold": 0.80
}
}
Plan-Specific Configuration
# Consumption Plan: limit maximum scalability
az functionapp config appsettings set \
--name func-myapp \
--resource-group rg-myapp \
--settings \
"WEBSITE_MAX_DYNAMIC_APPLICATION_SCALE_OUT=50" # Max 50 instances
# Premium Plan: configure minimum and maximum instances
az functionapp plan update \
--name asp-myapp-premium \
--resource-group rg-myapp \
--min-instances 2 \
--max-burst 20
# Flex Consumption: configure "always ready" instances
az functionapp config appsettings set \
--name func-myapp \
--resource-group rg-myapp \
--settings \
"WEBSITE_ALWAYS_ON=1" \
"functionsAlwaysReadyConfig=[{\"name\":\"HttpTriggers\",\"instanceCount\":2}]"
Module 6 – Durable Functions for Long-Running Workloads {#module-6}
Why Durable Functions for Performance?
Standard functions have a maximum timeout (10 min on Consumption). For processes that last longer, Durable Functions is the solution — it supports workflows lasting days, weeks, or months.
flowchart TD
subgraph "Without Durable Functions (Problem)"
BF[Standard function] -->|Timeout 10 min| TK[💀 Timeout!\nProcessing lost]
TK --> Restart[Restart from scratch\n⏱ Resource waste]
end
subgraph "With Durable Functions (Solution)"
DF[Orchestrator] -->|Persists state| Storage["(Azure Storage)"]
DF -->|Activity 1\n2 min| A1[✅]
A1 -->|Persist| Storage
DF -->|Activity 2\n5 min| A2[✅]
A2 -->|Persist| Storage
DF -->|External wait\n3 days| EXT[External Event]
EXT -->|Resume| DF
DF -->|Activity 3| A3[✅ Done]
end
style A1 fill:#107C10,color:#fff
style A2 fill:#107C10,color:#fff
style A3 fill:#107C10,color:#fff
style TK fill:#D83B01,color:#fff
Automatic Retry in Durable Functions
using Microsoft.Azure.Functions.Worker;
using Microsoft.DurableTask;
using Microsoft.Extensions.Logging;
namespace OrderPlatform.Durable;
public class ResilientWorkflow
{
private readonly ILogger<ResilientWorkflow> _logger;
public ResilientWorkflow(ILogger<ResilientWorkflow> logger)
{
_logger = logger;
}
[Function(nameof(ResilientWorkflow))]
public async Task<string> RunOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var orderId = context.GetInput<string>()!;
var logger = context.CreateReplaySafeLogger(nameof(ResilientWorkflow));
// Configure retry policies
var retryOptions = new TaskOptions(
new TaskRetryOptions(
new RetryPolicy(
maxNumberOfAttempts: 3,
firstRetryInterval: TimeSpan.FromSeconds(5),
backoffCoefficient: 2.0, // 5s, 10s, 20s
maxRetryInterval: TimeSpan.FromSeconds(30),
retryTimeout: TimeSpan.FromMinutes(5))));
try
{
// Activity with automatic retry
var result = await context.CallActivityAsync<ProcessResult>(
nameof(ProcessWithExternalAPI),
orderId,
retryOptions); // ← Automatic retry on failure!
logger.LogInformation(
"Workflow complete for order {OrderId}: {Result}",
orderId, result.Message);
return result.Message;
}
catch (TaskFailedException ex)
{
logger.LogError(
"Workflow failed after 3 attempts for order {OrderId}: {Error}",
orderId, ex.Message);
// Compensation
await context.CallActivityAsync(nameof(CancelOrder), orderId);
return $"Order {orderId} cancelled after failure";
}
}
[Function(nameof(ProcessWithExternalAPI))]
public async Task<ProcessResult> ProcessWithExternalAPI(
[ActivityTrigger] string orderId,
FunctionContext ctx)
{
_logger.LogInformation("Calling external API for order {OrderId}", orderId);
// Simulate an external API that sometimes fails
if (Random.Shared.Next(3) == 0)
throw new HttpRequestException("API temporarily unavailable");
return new ProcessResult($"Order {orderId} processed");
}
[Function(nameof(CancelOrder))]
public async Task CancelOrder([ActivityTrigger] string orderId, FunctionContext ctx)
{
_logger.LogWarning("Cancelling order {OrderId}", orderId);
// Cancellation/compensation logic
}
}
public record ProcessResult(string Message);
Module 7 – Monitoring and Cost Analysis {#module-7}
Cost Analysis with Azure Cost Management
flowchart LR
FA[Function App\nExecutions] -->|Metrics| AI[Application Insights]
FA -->|Costs| CM[Azure Cost Management]
AI -->|Analysis| Dashboard[KQL Dashboard]
CM -->|Budget Alerts| Alert[🔔 Budget Alerts]
style FA fill:#68217A,color:#fff
style Alert fill:#FFB900,color:#000
KQL Queries to Optimize Costs
// Analyze the most expensive functions
requests
| where timestamp > ago(30d)
| summarize
TotalExecutions = count(),
TotalDurationMs = sum(duration),
AvgDurationMs = avg(duration),
EstimatedCostUSD = sum(duration) * 0.000016 / 1000 // Rough estimate
by name
| extend TotalDurationSec = TotalDurationMs / 1000
| order by EstimatedCostUSD desc
| take 20
// Identify long-running functions (candidates for Durable Functions)
requests
| where timestamp > ago(7d)
| where duration > 60000 // More than 1 minute
| summarize
LongRunCount = count(),
MaxDurationMin = max(duration) / 60000,
P99DurationMin = percentile(duration, 99) / 60000
by name
| order by LongRunCount desc
// Identify functions with many failures (cost + degradation)
requests
| where timestamp > ago(7d)
| summarize
FailureCount = countif(success == false),
SuccessCount = countif(success == true),
FailureRate = round(countif(success == false) * 100.0 / count(), 2)
by name
| where FailureCount > 100
| order by FailureRate desc
// Analyze memory usage (for plan sizing)
performanceCounters
| where timestamp > ago(24h)
| where category == "Process" and counter == "Private Bytes"
| summarize
AvgMemoryMB = avg(value) / 1048576,
MaxMemoryMB = max(value) / 1048576,
P95MemoryMB = percentile(value, 95) / 1048576
| project AvgMemoryMB, MaxMemoryMB, P95MemoryMB
Configuring Cost Alerts
# Create a monthly budget alert
az consumption budget create \
--budget-name "azure-functions-monthly-budget" \
--amount 100 \
--time-grain Monthly \
--resource-group rg-myapp \
--notifications \
"threshold=80,contactEmails=ops@company.com,operator=GreaterThan" \
"threshold=100,contactEmails=cto@company.com,operator=GreaterThanOrEqualTo"
# Alert on abnormally high Function executions
az monitor metrics alert create \
--name "FunctionHighExecutionAlert" \
--resource-group rg-myapp \
--scopes $(az functionapp show --name func-myapp --resource-group rg-myapp --query id -o tsv) \
--condition "count requests > 1000000" \
--window-size 1h \
--evaluation-frequency 15m \
--description "More than 1 million executions in 1 hour (anomaly!)"
Custom Cost Dashboard
// Azure Monitor Workbook - Daily cost estimation
let GBSecPrice = 0.000016; // Price per GB-sec (approximate)
let ExecPrice = 0.0000002; // Price per execution
let FreeGBSec = 400000.0; // Monthly free quota
let FreeExec = 1000000.0; // Monthly free quota
requests
| where timestamp > ago(1d)
| summarize
TotalExecutions = count(),
TotalDurationMs = sum(duration),
AvgMemoryMB = 256 // Estimate; actual via perf counters
by bin(timestamp, 1h), name
| extend
GBSec = TotalDurationMs / 1000 * (AvgMemoryMB / 1024),
DailyExecCost = max_of(TotalExecutions - FreeExec/30, 0) * ExecPrice,
DailyGBSecCost = max_of(GBSec - FreeGBSec/30, 0) * GBSecPrice
| summarize
TotalDailyCost = sum(DailyExecCost + DailyGBSecCost)
by bin(timestamp, 1h)
| render timechart
Module 8 – Advanced Optimization {#module-8}
Optimizing JSON Serialization
// Use System.Text.Json with optimized options
// and streaming deserialization for large payloads
using System.Net;
using System.Text.Json;
using Microsoft.Azure.Functions.Worker.Http;
public class OptimizedJsonFunction
{
// Reuse JsonSerializerOptions (avoids re-creation on each call)
private static readonly JsonSerializerOptions CachedOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
WriteIndented = false // No indentation in production = fewer bytes
};
[Function("OptimizedJson")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
{
// Deserialization via ReadFromJsonAsync (optimized, streaming)
var request = await req.ReadFromJsonAsync<OrderRequest>(CachedOptions);
// Serialization with cached options
var responseData = new OrderResponse { Id = Guid.NewGuid(), Status = "Accepted" };
var response = req.CreateResponse(HttpStatusCode.Created);
response.Headers.Add("Content-Type", "application/json");
await response.WriteStringAsync(
JsonSerializer.Serialize(responseData, CachedOptions));
return response;
}
}
Memory Optimization with ArrayPool
using System.Buffers;
public class MemoryOptimizedFunction
{
[Function("ProcessLargeData")]
public async Task Run(
[BlobTrigger("uploads/{name}", Connection = "AzureWebJobsStorage")]
Stream blobStream,
string name)
{
// ✅ Use ArrayPool to avoid repeated allocations
const int bufferSize = 65536; // 64 KB
byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
try
{
int bytesRead;
long totalBytes = 0;
while ((bytesRead = await blobStream.ReadAsync(buffer, 0, bufferSize)) > 0)
{
totalBytes += bytesRead;
// Process buffer data...
await ProcessChunkAsync(buffer, bytesRead);
}
Console.WriteLine($"File {name} processed: {totalBytes} bytes");
}
finally
{
// IMPORTANT: Return the buffer to the pool!
ArrayPool<byte>.Shared.Return(buffer);
}
}
private static Task ProcessChunkAsync(byte[] buffer, int length)
{
// Chunk processing...
return Task.CompletedTask;
}
}
Module 9 – Performance Checklist {#module-9}
Complete Optimization Checklist
| Category | Optimization | Impact | Ease |
|---|---|---|---|
| Cold Start | Reduce NuGet packages | ⭐⭐⭐ | Easy |
| Cold Start | Always-ready instances (Flex/Premium) | ⭐⭐⭐ | Medium |
| Cold Start | Singleton for clients (Cosmos, HTTP) | ⭐⭐⭐ | Easy |
| Memory | ArrayPool for large buffers | ⭐⭐ | Medium |
| Memory | JSON Source Generators | ⭐⭐ | Medium |
| Memory | Avoid large in-memory collections | ⭐⭐⭐ | Easy |
| CPU | async/await everywhere (avoid .Result) | ⭐⭐⭐ | Easy |
| CPU | Parallelism with Task.WhenAll | ⭐⭐⭐ | Easy |
| CPU | DbContextPool instead of DbContext | ⭐⭐ | Easy |
| Network | HttpClient via IHttpClientFactory | ⭐⭐⭐ | Easy |
| Network | Connection pooling (CosmosClient singleton) | ⭐⭐⭐ | Easy |
| Scaling | Small and focused functions | ⭐⭐⭐ | Easy |
| Scaling | Avoid functions > 2 min (Consumption) | ⭐⭐⭐ | Medium |
| Cost | Run-From-Package instead of classic deployment | ⭐⭐ | Easy |
| Cost | Daily quota (Consumption) to prevent surprises | ⭐⭐ | Easy |
| Resilience | Idempotence on queue functions | ⭐⭐⭐ | Medium |
| Resilience | Retry policies with Polly | ⭐⭐⭐ | Medium |
| Resilience | Dead-letter queue monitoring | ⭐⭐⭐ | Easy |
Quick Diagnostic Commands
# Check health and metrics of a Function App
FUNC_APP="func-myapp-prod"
RG="rg-myapp"
echo "=== General Status ==="
az functionapp show \
--name $FUNC_APP \
--resource-group $RG \
--query '{status:state, plan:serverFarmId, tier:sku.tier}' \
--output table
echo "=== Recent Errors ==="
az monitor activity-log list \
--resource-group $RG \
--status Failed \
--offset 1h \
--query '[].{time:eventTimestamp, operation:operationName.localizedValue, status:status.localizedValue}' \
--output table
echo "=== Metrics for Last 30 Minutes ==="
az monitor metrics list \
--resource $(az functionapp show --name $FUNC_APP --resource-group $RG --query id -o tsv) \
--metric "FunctionExecutionCount" "FunctionExecutionUnits" "Http5xx" \
--interval PT5M \
--aggregation Total \
--output table
echo "=== App Settings (without secret values) ==="
az functionapp config appsettings list \
--name $FUNC_APP \
--resource-group $RG \
--query '[].name' \
--output table
Glossary {#glossary}
| Term | Definition |
|---|---|
| Always-Ready Instance | Pre-warmed instance that avoids cold starts (Flex Consumption, Premium) |
| ArrayPool | .NET array pool to reduce memory allocations (GC pressure) |
| Exponential Backoff | Increasing wait between retries (2s, 4s, 8s) to avoid overload |
| Circuit Breaker | Polly pattern that cuts calls to a failing service |
| Cold Start | Startup delay of a new Function instance with no recent activity |
| Dynamic Concurrency | Mode where Azure automatically adjusts concurrency based on CPU/memory load |
| Consumption Plan | Azure Functions serverless plan, strictly usage-based billing |
| DB Context Pool | EF Core DbContext pool reducing the cost of connection creation |
| Dedicated Plan | Traditional App Service plan for Azure Functions |
| Dead-Letter Queue | Queue receiving messages that cannot be processed after N attempts |
| Durable Functions | Azure Functions extension for stateful and long-running workflows |
| Flex Consumption | Hybrid plan: serverless + VNet + configurable memory |
| GB-Sec | Billing unit: 1 GB of RAM used for 1 second |
| HttpClientFactory | .NET factory managing an HttpClient pool to avoid socket exhaustion |
| Idempotence | Property: same operation N times = same result as 1 time |
| ILogger | .NET standard interface for structured logging |
| Jitter | Random variation in retry delays to avoid “thundering herd” |
| JSON Source Generator | Compile-time generation of JSON serialization code (faster) |
| KQL | Kusto Query Language - Azure Monitor / Application Insights query language |
| Polly | .NET resilience library (retry, circuit breaker, timeout, bulkhead) |
| Premium Plan | Azure Functions plan with pre-warmed instances and VNet |
| Request Unit (RU) | Cosmos DB throughput unit |
| Scale to Zero | Ability to reduce to 0 active instances (Consumption, Flex) |
| Singleton | DI pattern: one instance created and reused |
| Thundering Herd | Scenario where many instances retry simultaneously, overloading a service |
| Timeout | Maximum execution duration of a function |
| Warm Pool | Pool of Azure pre-allocated instances to reduce cold starts |
Supplementary Reference
Hosting Plans — Detailed Comparison
5 hosting options and their characteristics:
| Plan | Cold Start | Max timeout | Memory | VNet | Cost |
|---|---|---|---|---|---|
| Consumption | Yes | 10 min | 1.5 GB | ❌ | Pay per exec |
| Flex Consumption | Reduced | 60 min | 512 MB - 4 GB | ✅ | Pay per exec + always-ready |
| Premium | ❌ (pre-warmed) | Unlimited | 3.5 GB - 14 GB | ✅ | Per hour (active instance) |
| Dedicated (App Service) | ❌ | Unlimited | Per SKU | ✅ | Fixed App Service Plan |
| Container Apps | Per KEDA config | Unlimited | Configurable | ✅ | Per vCPU/memory |
Cold Start — Anatomy and Solutions
What is a cold start?
When a function is called after a period of inactivity, Azure must create a new instance.
The 4 stages of a cold start:
Stage 1: Find an available physical host
↓ (100-500ms)
Stage 2: Create the Azure Function resource (container/VM)
↓ (200-1000ms)
Stage 3: Load the Function runtime + dependencies
(the .NET runtime, your NuGet packages)
↓ (500ms - several seconds)
Stage 4: Execute your code
↓ (your business logic)
Total cold start: 1 to 10+ seconds
Cold start reduction strategies:
| Strategy | Plan | Description |
|---|---|---|
| Always-ready instances | Flex Consumption | Keep N instances always active |
| Pre-warmed workers | Premium | Pre-configured instances in the pool |
| Ping timer | Consumption | Timer trigger every 5-10 min to stay warm |
| Dedicated plan | Dedicated | Always active, never cold start |
Architecture Best Practices
Small and specific functions
❌ Bad — one function that does everything:
OrderProcessingFunction:
→ Validate the order
→ Check inventory
→ Take payment
→ Create shipment
→ Send confirmation
→ Update analytics
✅ Good — separate functions:
ValidateOrderFunction
CheckInventoryFunction
ProcessPaymentFunction
CreateShipmentFunction
SendConfirmationFunction
UpdateAnalyticsFunction
Stateless design
// ❌ Bad — mutable global state
private static int _counter = 0; // Shared between invocations!
[FunctionName("BadFunction")]
public static void Run(...)
{
_counter++; // Race condition with scale-out
}
// ✅ Good — stateless, state in external storage
[FunctionName("GoodFunction")]
public static async Task Run(
[HttpTrigger] HttpRequest req,
[CosmosDB("db", "counters")] IAsyncCollector<Counter> outputCounters)
{
await outputCounters.AddAsync(new Counter { Value = 1, Timestamp = DateTime.UtcNow });
}
Hardening and Resilience Reference
Retry Logic
// host.json — configure retry policies
{
"extensions": {
"queues": {
"maxDequeueCount": 5, // Retry 5 times before DLQ
"visibilityTimeout": "00:05:00" // Delay between retries
}
}
}
Exponential backoff retry:
[FunctionName("ResilientFunction")]
[FixedDelayRetry(5, "00:00:30")] // 5 retries, 30s between each
public static async Task Run(
[QueueTrigger("orders")] string order,
ILogger log)
{
// If exception → automatic retry
await ProcessOrder(order);
}
Timeout on external calls:
public static async Task Run(...)
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
try
{
var result = await externalService.GetDataAsync(cts.Token);
}
catch (OperationCanceledException)
{
log.LogWarning("External call timed out after 30s");
// Handle gracefully (retry, fallback, etc.)
}
}
Durable Functions for Long-Running Reference
Why Durable Functions for long-running processes:
Problem with normal functions for long-running:
❌ Timeout (max 10 min in Consumption)
❌ If scale-down, state is lost
❌ No resume after crash
Solution — Durable Functions:
✅ State persisted in Azure Storage (Table + Blobs)
✅ Automatic resume after crash (replay pattern)
✅ Unlimited timeout (orchestration can last days)
✅ State visibility via Azure Portal
Cost estimation for Consumption:
Price: $0.20 / million executions
$0.000016 / GB-s (memory × duration)
Example:
1 million executions / month
× 256 MB × 0.5 second average = 128,000 GB-s / month
→ Cost = $0.20 + ($0.000016 × 128,000) = $0.20 + $2.05 = $2.25 / month
Optimization Checklist
Performance:
□ Choose the right plan based on needs (is cold start acceptable?)
□ Configure always-ready if using Flex Consumption
□ Reduce NuGet dependencies (cold start)
□ Small and specific functions
□ Avoid long-running operations (use Durable)
Cost:
□ Consumption for sporadic workload
□ Dedicated for constant workload
□ Configure max scale to avoid unexpected overages
□ Monitor costs with Cost Management + budgets/alerts
Resilience:
□ Retry logic with exponential backoff
□ Timeouts on all external calls
□ Dead Letter Queue for failed messages
□ Stateless design (state in Cosmos DB / Storage)
Search Terms
optimizing · azure · functions · performance · cost · serverless · microsoft · cold · plan · optimization · durable · resilience · checklist · comparison · configuration · consumption · hosting · long-running · plans · reference · retry · scaling · analysis · anatomy