Level: Intermediate to Advanced
Technologies: Azure Functions v4 / .NET 8 / C# / Isolated Worker Model
About this guide: This document is a comprehensive reference for designing serverless architectures with Azure Functions. It covers architectural patterns, integration with Azure services, resilience, performance, and security best practices. Designed for cloud architects and developers.
Table of Contents
- Serverless and Event-Driven Architecture
- Hosting Plans and Scalability Strategies
- Integration with Azure Services
- Resilience, Performance, and High Availability
- Monitoring and Observability
- Security and Governance
- Advanced Design Patterns
- Complete Reference Architecture
- Best Practices and Pitfalls to Avoid
- Glossary
Module 1 – Serverless and Event-Driven Architecture {#module-1}
The Serverless Paradigm: Understanding Statelessness
In a serverless architecture, functions are designed to wake up, complete a single task, and shut down — without needing to remember what happened before or what comes next. This is the essence of statelessness. And it is precisely this stateless design that makes Azure Functions ideal for highly scalable event-driven systems.
Why statelessness is a strength, not a limitation:
The platform can instantiate thousands of instances across regions without coordination, because each invocation is completely isolated — it retains no context from a previous execution. Persistent state must instead be externalized to storage services (Azure Storage, Cosmos DB, Redis Cache) rather than maintained in memory.
flowchart LR
subgraph "Stateless Function Execution"
R1[Request 1] --> F1[Instance Function A]
R2[Request 2] --> F2[Instance Function B]
R3[Request 3] --> F3[Instance Function C]
R4[Request 4] --> F4[Instance Function D]
F1 & F2 & F3 & F4 --> DB["(External State\nAzure Storage\nCosmos DB)"]
end
style DB fill:#0078D4,color:#fff
Event-Driven Architecture: The Core Components
Event-driven architecture is at the heart of how Azure Functions operates. The system consists of three main elements:
flowchart TD
P1[Producer 1\nUser action] -->|Emits an event| EC[Event Channel\nEvent Grid / Service Bus / Queue]
P2[Producer 2\nSystem change] -->|Emits an event| EC
P3[Producer 3\nExternal service] -->|Emits an event| EC
EC -->|Distributes| C1[Consumer 1\nAzure Function]
EC -->|Distributes| C2[Consumer 2\nAzure Function]
EC -->|Distributes| C3[Consumer 3\nLogic App]
style EC fill:#0078D4,color:#fff
style C1 fill:#68217A,color:#fff
style C2 fill:#68217A,color:#fff
| Component | Role | Azure Examples |
|---|---|---|
| Producer | Generates and emits events | Web app, external API, IoT device, timer |
| Event Channel | Transports and routes events | Azure Event Grid, Service Bus, Storage Queue, Event Hubs |
| Consumer | Reacts to events in isolation | Azure Functions, Logic Apps, webhooks |
Pub/Sub Pattern (Publish-Subscribe)
In the Pub/Sub model, a single event can trigger multiple independent consumers. This is particularly useful when several services need to react differently to the same event.
Concrete example: New user registration
Azure Event Grid (Topic: "user-events")
↓ Event: { "type": "UserRegistered", "userId": "abc123", "email": "user@example.com" }
├── Function 1: Send welcome email (SendGrid)
├── Function 2: Log the analytics event (Application Insights)
├── Function 3: Create the initial user profile (Cosmos DB)
└── Function 4: Notify the sales team (Slack webhook)
// Consumer 1: Sending the welcome email
using Azure.Messaging.EventGrid;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace ShopTicket.Functions;
public class WelcomeEmailFunction
{
private readonly ILogger<WelcomeEmailFunction> _logger;
private readonly IEmailService _emailService;
public WelcomeEmailFunction(
ILogger<WelcomeEmailFunction> logger,
IEmailService emailService)
{
_logger = logger;
_emailService = emailService;
}
[Function("SendWelcomeEmail")]
public async Task Run(
[EventGridTrigger] EventGridEvent eventGridEvent)
{
_logger.LogInformation("Event received: {EventType}", eventGridEvent.EventType);
if (eventGridEvent.EventType != "UserRegistered")
return;
var userData = eventGridEvent.Data.ToObjectFromJson<UserRegisteredEvent>();
await _emailService.SendWelcomeEmailAsync(
userData.Email,
userData.FirstName);
_logger.LogInformation("Welcome email sent to {Email}", userData.Email);
}
}
public record UserRegisteredEvent(string UserId, string Email, string FirstName);
Event Streaming Pattern
Event streaming is used for high-volume continuous data flows. Azure Event Hubs is the preferred solution for IoT, telemetry, and real-time analytics scenarios.
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace ShopTicket.Functions;
public class TelemetryProcessorFunction
{
private readonly ILogger<TelemetryProcessorFunction> _logger;
private readonly IAnalyticsService _analyticsService;
public TelemetryProcessorFunction(
ILogger<TelemetryProcessorFunction> logger,
IAnalyticsService analyticsService)
{
_logger = logger;
_analyticsService = analyticsService;
}
/// <summary>
/// Batch processing of Event Hubs events.
/// A batch can contain up to 10 events (configurable).
/// </summary>
[Function("ProcessTelemetry")]
public async Task Run(
[EventHubTrigger(
"telemetry-hub",
Connection = "EventHubConnection",
IsBatched = true)]
IReadOnlyList<SensorReading> sensorReadings,
FunctionContext context)
{
_logger.LogInformation(
"Processing a batch of {Count} sensor readings",
sensorReadings.Count);
// Parallel batch processing
var tasks = sensorReadings.Select(reading =>
_analyticsService.ProcessReadingAsync(reading));
await Task.WhenAll(tasks);
// Aggregate statistics
var avgTemperature = sensorReadings.Average(r => r.Temperature);
var maxPressure = sensorReadings.Max(r => r.Pressure);
_logger.LogInformation(
"Batch processed. Average temperature: {AvgTemp:F2}°C, Max pressure: {MaxPressure:F2} hPa",
avgTemperature, maxPressure);
}
}
public record SensorReading(
string SensorId,
double Temperature,
double Pressure,
double Humidity,
DateTime Timestamp);
Durable Functions – Stateful Workflows
While stateless functions offer speed and scalability, Durable Functions introduce stateful workflows that allow complex, multi-step operations to be coordinated reliably.
stateDiagram-v2
[*] --> Pending: Order received
Pending --> CheckingInventory: Start inventory check
CheckingInventory --> InsufficientStock: Insufficient stock
CheckingInventory --> ProcessingPayment: Stock available
InsufficientStock --> [*]: Cancellation
ProcessingPayment --> PaymentFailed: Payment failed
ProcessingPayment --> SendingConfirmation: Payment succeeded
PaymentFailed --> [*]: Refund if necessary
SendingConfirmation --> Completed: Email sent
Completed --> [*]: Success
The four main Durable Functions patterns:
| Pattern | Description | Use Case |
|---|---|---|
| Chaining | Sequential steps, each depends on the previous | E-commerce order processing |
| Fan-out/Fan-in | Parallel activities + wait for all | Bulk email sending, report generation |
| Async HTTP API | Start a long process + status polling | Video transcription, image analysis |
| Monitor | Periodic polling until a condition is met | Monitor availability of an external service |
| Human Interaction | Wait for human approval with timeout | Suspicious order validation |
| Aggregator | Aggregate events over a period | Daily sales metrics |
Module 2 – Hosting Plans and Scalability Strategies {#module-2}
Detailed Comparison of Hosting Plans
The choice of hosting plan is fundamental. It determines how your application scales, its startup speed, cost, and available features.
flowchart TD
A{Which plan to choose?} --> B{Predictable\nand critical workload?}
B -->|Yes| C{Budget available?}
B -->|No| D[Consumption Plan\n$$ Pay-per-use]
C -->|Limited| E[Flex Consumption\n$$$ Optimal compromise]
C -->|Available| F{Need VNet/\nPrivate Endpoints?}
F -->|Yes| G[Premium Plan\n$$$$ Maximum performance]
F -->|No| H{Existing\nApp Service infra?}
H -->|Yes| I[Dedicated Plan\n$$$$ Shares infra]
H -->|No| J{Containerized?}
J -->|Yes| K[Container Apps\n$$$$$ Native Kubernetes]
J -->|No| G
style D fill:#107C10,color:#fff
style E fill:#0078D4,color:#fff
style G fill:#68217A,color:#fff
| Feature | Consumption | Flex Consumption | Premium EP1 | Premium EP2 | Dedicated | Container Apps |
|---|---|---|---|---|---|---|
| Billing model | Pay-per-use | Pay-per-use + instances | Instances + scaling | Instances + scaling | Monthly plan | Kubernetes |
| Cold Starts | Yes (2-5s) | Low | No (pre-warmed) | No | No | Variable |
| Default Timeout | 5 min | 30 min | 30 min | 30 min | Unlimited | Unlimited |
| Max Timeout | 10 min | Unlimited | Unlimited | Unlimited | Unlimited | Unlimited |
| VNet Integration | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Private Endpoints | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
| vCPU (instance) | Shared | 1 vCPU | 1 vCPU | 2 vCPU | Variable | Variable |
| RAM (instance) | 1.5 GB | 2 GB | 3.5 GB | 7 GB | Variable | Variable |
| Max Scale Out | 200 instances | Variable | 100 instances | 100 instances | Per plan | Unlimited |
| Scale to Zero | ✅ | ✅ | ❌ (min 1) | ❌ (min 1) | ❌ | ✅ |
| Free quota | 1M exec/400K GB-sec | No | No | No | No | No |
Scaling Configuration
Consumption Plan: Automatic Behavior
// host.json - Scaling tuning for Consumption plan
{
"version": "2.0",
"extensions": {
"queues": {
"batchSize": 16,
"newBatchThreshold": 8,
"maxDequeueCount": 5,
"maxPollingInterval": "00:00:02"
},
"serviceBus": {
"maxConcurrentCalls": 16,
"maxConcurrentSessions": 8,
"prefetchCount": 50
},
"eventHubs": {
"batchCheckpointFrequency": 5,
"eventProcessorOptions": {
"maxBatchSize": 100,
"prefetchCount": 300
}
}
},
"functionTimeout": "00:10:00",
"healthMonitor": {
"enabled": true,
"healthCheckInterval": "00:00:10",
"healthCheckWindow": "00:02:00",
"healthCheckThreshold": 6,
"counterThreshold": 0.80
}
}
Premium Plan: Dynamic Concurrency
// host.json - Dynamic concurrency (Premium Plan)
{
"version": "2.0",
"concurrency": {
"dynamicConcurrencyEnabled": true,
"snapshotPersistenceEnabled": true
}
}
Essential Application Settings Configuration
# Complete configuration for a production Function App
az functionapp config appsettings set \
--name func-shopticket-prod \
--resource-group rg-shopticket-prod \
--settings \
"FUNCTIONS_EXTENSION_VERSION=~4" \
"FUNCTIONS_WORKER_RUNTIME=dotnet-isolated" \
"DOTNET_ISOLATED_JITCACHING=1" \
"WEBSITE_RUN_FROM_PACKAGE=1" \
"WEBSITE_CONTENTAZUREFILECONNECTIONSTRING=@Microsoft.KeyVault(...)" \
"APPLICATIONINSIGHTS_CONNECTION_STRING=@Microsoft.KeyVault(...)" \
"ASPNETCORE_ENVIRONMENT=Production" \
"WEBSITE_MAX_DYNAMIC_APPLICATION_SCALE_OUT=20"
Deployment Slots
Deployment slots enable zero-downtime updates with the ability to swap environments:
# Create a staging slot
az functionapp deployment slot create \
--name func-shopticket-prod \
--resource-group rg-shopticket-prod \
--slot staging
# Deploy to the staging slot
az functionapp deployment source config-zip \
--name func-shopticket-prod \
--resource-group rg-shopticket-prod \
--slot staging \
--src ./functions-app.zip
# Test the staging slot
# URL: https://func-shopticket-prod-staging.azurewebsites.net
# Swap staging → production
az functionapp deployment slot swap \
--name func-shopticket-prod \
--resource-group rg-shopticket-prod \
--slot staging \
--target-slot production
Module 3 – Integration with Azure Services {#module-3}
Integration with Azure Event Grid
Azure Event Grid is Azure’s native event routing service. It uses a publish/subscribe model that delivers events from producers to consumers in near real-time.
sequenceDiagram
participant BS as Azure Blob Storage
participant EG as Azure Event Grid
participant AF1 as Function: GenerateIndex
participant AF2 as Function: NotifyUsers
participant AF3 as Function: UpdateCDN
BS->>EG: BlobCreated event\n{ blobUrl, contentType, size }
EG->>AF1: Filtered by: subject contains "/products/"
EG->>AF2: Filtered by: eventType = "Microsoft.Storage.BlobCreated"
EG->>AF3: Filtered by: data.contentType = "image/*"
AF1-->>EG: 200 OK (processed)
AF2-->>EG: 200 OK (processed)
AF3-->>EG: 200 OK (processed)
Key Event Grid concepts:
| Concept | Description | Example |
|---|---|---|
| Event Source | Service that emits events | Blob Storage, Resource Group, Custom Topic |
| Topic | Endpoint where events are sent | System Topics (automatic) or Custom Topics |
| Event Type | Category of the event | Microsoft.Storage.BlobCreated, UserRegistered |
| Subscription | Filtering and delivery rule | Filter by type, subject, or custom attributes |
| Handler | Recipient of the event | Azure Function, Logic App, Event Hub, Webhook |
using Azure.Messaging.EventGrid;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using System.Text.Json;
namespace ShopTicket.Functions;
public class ProductCatalogEventHandler
{
private readonly ILogger<ProductCatalogEventHandler> _logger;
private readonly ISearchIndexService _searchService;
private readonly ICdnService _cdnService;
public ProductCatalogEventHandler(
ILogger<ProductCatalogEventHandler> logger,
ISearchIndexService searchService,
ICdnService cdnService)
{
_logger = logger;
_searchService = searchService;
_cdnService = cdnService;
}
/// <summary>
/// Reacts to product create/update events.
/// Triggered by Azure Event Grid.
/// </summary>
[Function("HandleProductEvent")]
public async Task Run(
[EventGridTrigger] EventGridEvent eventGridEvent)
{
_logger.LogInformation(
"Event received: {EventType} for {Subject}",
eventGridEvent.EventType,
eventGridEvent.Subject);
switch (eventGridEvent.EventType)
{
case "ProductCreated":
case "ProductUpdated":
var product = eventGridEvent.Data.ToObjectFromJson<ProductData>();
await HandleProductUpsertAsync(product, eventGridEvent.EventType);
break;
case "ProductDeleted":
var deletedProduct = eventGridEvent.Data.ToObjectFromJson<ProductDeletedData>();
await HandleProductDeletionAsync(deletedProduct.ProductId);
break;
case "Microsoft.Storage.BlobCreated":
var blobData = eventGridEvent.Data.ToObjectFromJson<StorageBlobCreatedEventData>();
if (blobData.Url.Contains("/product-images/"))
{
await _cdnService.InvalidateCacheAsync(blobData.Url);
}
break;
default:
_logger.LogWarning("Unhandled event type: {EventType}", eventGridEvent.EventType);
break;
}
}
private async Task HandleProductUpsertAsync(ProductData product, string eventType)
{
_logger.LogInformation(
"{EventType}: Product {ProductId} - {Name}",
eventType, product.ProductId, product.Name);
await _searchService.UpsertProductAsync(product);
_logger.LogInformation("Search index updated for product {ProductId}", product.ProductId);
}
private async Task HandleProductDeletionAsync(string productId)
{
_logger.LogInformation("Removing product {ProductId} from index", productId);
await _searchService.DeleteProductAsync(productId);
}
}
// Publish a custom event to Event Grid
public class ProductEventPublisher
{
private readonly EventGridPublisherClient _publisherClient;
public ProductEventPublisher(EventGridPublisherClient publisherClient)
{
_publisherClient = publisherClient;
}
public async Task PublishProductCreatedAsync(ProductData product)
{
var eventData = new EventGridEvent(
subject: $"/products/{product.ProductId}",
eventType: "ProductCreated",
dataVersion: "1.0",
data: new BinaryData(product));
await _publisherClient.SendEventAsync(eventData);
}
}
Integration with Azure Cosmos DB
Azure Cosmos DB integrates natively with Azure Functions via its Change Feed — a real-time stream that captures inserts and updates in Cosmos DB containers.
flowchart LR
App[Application] -->|Insert/Update| Cosmos["(Cosmos DB\nContainer: Orders)"]
Cosmos -->|Change Feed| AF[Azure Function\nCosmosDB Trigger]
AF -->|Syncs| Cache["(Redis Cache)"]
AF -->|Updates| Search[Azure Cognitive Search]
AF -->|Sends notification| SB[Service Bus]
style Cosmos fill:#0078D4,color:#fff
style AF fill:#68217A,color:#fff
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace ShopTicket.Functions;
public class OrderChangeFeedProcessor
{
private readonly ILogger<OrderChangeFeedProcessor> _logger;
private readonly ICacheService _cacheService;
private readonly INotificationService _notificationService;
public OrderChangeFeedProcessor(
ILogger<OrderChangeFeedProcessor> logger,
ICacheService cacheService,
INotificationService notificationService)
{
_logger = logger;
_cacheService = cacheService;
_notificationService = notificationService;
}
/// <summary>
/// Triggered automatically by the Cosmos DB Change Feed.
/// Processes changes in batches to optimize performance.
/// </summary>
[Function("ProcessOrderChanges")]
public async Task Run(
[CosmosDBTrigger(
databaseName: "ShopTicketDB",
containerName: "Orders",
Connection = "CosmosDBConnection",
LeaseContainerName = "Leases",
CreateLeaseContainerIfNotExists = true,
FeedPollDelay = 1000)] // Poll every 1 second
IReadOnlyList<OrderDocument> changedOrders)
{
if (changedOrders is null || changedOrders.Count == 0)
return;
_logger.LogInformation(
"Change Feed: {Count} order changes detected",
changedOrders.Count);
// Parallel processing with concurrency limit
var semaphore = new SemaphoreSlim(10); // Max 10 simultaneous operations
var tasks = changedOrders.Select(async order =>
{
await semaphore.WaitAsync();
try
{
// Update cache
await _cacheService.SetAsync(
$"order:{order.OrderId}",
order,
TimeSpan.FromMinutes(30));
// Notify if status changed to "Completed"
if (order.Status == "Completed")
{
await _notificationService.SendOrderCompletedNotificationAsync(order);
}
_logger.LogDebug(
"Order {OrderId} processed, status: {Status}",
order.OrderId, order.Status);
}
finally
{
semaphore.Release();
}
});
await Task.WhenAll(tasks);
_logger.LogInformation(
"Processing complete for {Count} changes",
changedOrders.Count);
}
}
Important Cosmos DB considerations:
| Aspect | Recommendation | Why |
|---|---|---|
| Request Units (RU) | Plan the RU budget based on Change Feed volume | Avoid throttling (429 Too Many Requests) |
| Partition Key | Choose partition key carefully | Major impact on cost and performance |
| Consistency Level | Session or Eventual for most cases | Strong consistency = higher latency + cost |
| Lease Container | One lease container per application | Coordination between Function instances |
| TTL (Time to Live) | Enable on temporary documents | Reduce storage costs |
Integration with Azure Storage Queues
Azure Storage Queues provide simple, reliable messaging for decoupling application components. The philosophy is clear: instead of directly calling another service, a function drops a message onto the queue, and another processes it asynchronously.
sequenceDiagram
participant HTTP as HTTP Function\n(Fast Response)
participant Q as Storage Queue
participant QF as Queue Function\n(Async Processing)
participant DB as Database
participant Email as Email Service
HTTP->>Q: Drop OrderRequest message
HTTP-->>Client: HTTP 202 Accepted (fast)
Note over Q: Message stored durably
Q->>QF: Trigger processing (≤ 30 sec visibility)
QF->>DB: Save order
QF->>Email: Send confirmation
QF-->>Q: Delete message (success)
alt Processing failure
QF-->>Q: Message becomes visible again (retry)
Note over Q,QF: Automatic retry up to maxDequeueCount (5)
Q-->QF: Last retry failed
Q->>QF: Move to poison queue
end
using Azure.Storage.Queues.Models;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace ShopTicket.Functions;
public class OrderQueueProcessor
{
private readonly ILogger<OrderQueueProcessor> _logger;
private readonly IOrderService _orderService;
public OrderQueueProcessor(
ILogger<OrderQueueProcessor> logger,
IOrderService orderService)
{
_logger = logger;
_orderService = orderService;
}
// Main order processing
[Function("ProcessOrderQueue")]
public async Task ProcessOrder(
[QueueTrigger("orders-queue", Connection = "AzureWebJobsStorage")]
OrderQueueMessage order)
{
_logger.LogInformation(
"Processing order {OrderId}, attempt #{DequeueCount}",
order.OrderId,
order.DequeueCount);
// Check idempotency
if (await _orderService.IsAlreadyProcessedAsync(order.OrderId))
{
_logger.LogWarning("Order {OrderId} already processed, skipping", order.OrderId);
return;
}
await _orderService.ProcessAsync(order);
}
// Handler for the poison queue (dead-letter)
[Function("HandlePoisonOrder")]
public async Task HandlePoison(
[QueueTrigger("orders-queue-poison", Connection = "AzureWebJobsStorage")]
OrderQueueMessage poisonOrder)
{
_logger.LogError(
"Order {OrderId} in poison queue after {Count} attempts. Manual intervention required.",
poisonOrder.OrderId,
poisonOrder.DequeueCount);
// Alert the ops team
await _orderService.FlagForManualReviewAsync(poisonOrder, "Poison queue - automatic processing failed");
}
}
Integration with Azure Service Bus
Azure Service Bus provides advanced enterprise messaging features (sessions, native dead-letter queue, deferred messages, transactions).
using Azure.Messaging.ServiceBus;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace ShopTicket.Functions;
public class ServiceBusOrderProcessor
{
private readonly ILogger<ServiceBusOrderProcessor> _logger;
public ServiceBusOrderProcessor(ILogger<ServiceBusOrderProcessor> logger)
{
_logger = logger;
}
/// <summary>
/// Processing Service Bus messages with sessions.
/// Sessions guarantee ordered processing for the same OrderId.
/// </summary>
[Function("ProcessServiceBusOrder")]
public async Task Run(
[ServiceBusTrigger(
"orders-topic/subscriptions/processing-service",
Connection = "ServiceBusConnection",
IsSessionsEnabled = true)] // Sessions for guaranteed processing order
ServiceBusReceivedMessage message,
ServiceBusMessageActions messageActions)
{
_logger.LogInformation(
"Service Bus message received. MessageId: {MessageId}, SessionId: {SessionId}",
message.MessageId,
message.SessionId);
try
{
var order = message.Body.ToObjectFromJson<OrderMessage>();
// Process the order
// ...
// Confirm processing (Complete)
await messageActions.CompleteMessageAsync(message);
_logger.LogInformation("Message {MessageId} processed successfully", message.MessageId);
}
catch (BusinessException ex)
{
_logger.LogWarning(ex, "Business error for message {MessageId}", message.MessageId);
// Dead-letter with explicit reason
await messageActions.DeadLetterMessageAsync(message,
deadLetterReason: "BusinessRuleViolation",
deadLetterErrorDescription: ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Technical error for message {MessageId}", message.MessageId);
// Abandon = message becomes visible again for retry
await messageActions.AbandonMessageAsync(message);
throw;
}
}
}
Module 4 – Resilience, Performance, and High Availability {#module-4}
Load Balancing and Fault Tolerance Patterns
Azure Functions handles load balancing automatically at the platform level. However, several parameters can be configured to optimize behavior under load.
Static vs. Dynamic Concurrency
flowchart TD
subgraph "Static Concurrency (manually configured)"
A1[Trigger: Service Bus] -->|maxConcurrentCalls: 16| B1[16 simultaneous processing\nper instance]
B1 -->|High CPU?| C1[Performance issue\nrequires manual tuning]
end
subgraph "Dynamic Concurrency (Azure manages)"
A2[Trigger: Service Bus] -->|dynamicConcurrencyEnabled: true| B2[Azure measures CPU/Memory]
B2 -->|Low load| C2[Increases concurrency]
B2 -->|High load| D2[Reduces concurrency]
C2 & D2 --> E2[Automatic optimal balance]
end
style E2 fill:#107C10,color:#fff
// Retry pattern with Polly (for external calls in Activity Functions)
using Polly;
using Polly.Extensions.Http;
// Program.cs - Retry policy configuration
services.AddHttpClient("PaymentProvider")
.AddPolicyHandler(GetRetryPolicy())
.AddPolicyHandler(GetCircuitBreakerPolicy());
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)) // Exponential backoff
+ TimeSpan.FromMilliseconds(new Random().Next(0, 100))); // Jitter
}
static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromSeconds(30));
}
Multi-Region High Availability Patterns
Active-Passive (recommended for non-HTTP triggers)
flowchart LR
subgraph "Primary Region (Active)"
FA1[Function App\nEast US]
Q1[Storage Queue\nEast US]
DB1["(Cosmos DB\nEast US)"]
end
subgraph "Secondary Region (Passive)"
FA2[Function App\nWest Europe]
Q2[Storage Queue\nWest Europe]
DB2["(Cosmos DB\nWest Europe\ngeo-replicated)"]
end
TM[Azure Traffic Manager\nor Azure Front Door] -.->|Failover if East US down| FA2
Q1 --> FA1
FA1 --> DB1
DB1 <-->|Geo-replication| DB2
style FA1 fill:#68217A,color:#fff
style FA2 fill:#888,color:#fff
style TM fill:#0078D4,color:#fff
Active-Active (recommended for HTTP triggers)
# Azure Front Door configuration for active-active load balancing
az afd endpoint create \
--resource-group rg-shopticket-prod \
--profile-name afd-shopticket \
--endpoint-name api-endpoint \
--enabled-state Enabled
# Add origins (Function Apps in two regions)
az afd origin create \
--resource-group rg-shopticket-prod \
--profile-name afd-shopticket \
--origin-group-name api-origins \
--origin-name east-us-origin \
--host-name func-shopticket-eastus.azurewebsites.net \
--priority 1 \
--weight 50
az afd origin create \
--resource-group rg-shopticket-prod \
--profile-name afd-shopticket \
--origin-group-name api-origins \
--origin-name west-europe-origin \
--host-name func-shopticket-westeurope.azurewebsites.net \
--priority 1 \
--weight 50
Load Testing with Azure Load Testing
# Test configuration summary for ShopTicket API
Test Configuration:
- Duration: 10 minutes
- Concurrent users: 1000
- Ramp-up: 2 minutes
Scenarios tested:
1. GET /api/products - 500 req/sec
2. POST /api/orders - 200 req/sec
3. GET /api/orders/{id} - 300 req/sec
Metrics to monitor:
- p95 response time < 500ms
- Error rate < 1%
- Throughput > 900 req/sec
- Function App CPU < 80%
// Performance optimization: using distributed cache
using Microsoft.Extensions.Caching.Distributed;
public class ProductsFunction
{
private readonly IDistributedCache _cache;
private readonly IProductRepository _productRepository;
private readonly ILogger<ProductsFunction> _logger;
public ProductsFunction(
IDistributedCache cache,
IProductRepository productRepository,
ILogger<ProductsFunction> logger)
{
_cache = cache;
_productRepository = productRepository;
_logger = logger;
}
[Function("GetProducts")]
public async Task<HttpResponseData> GetProducts(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "products")]
HttpRequestData req)
{
const string cacheKey = "all-products";
// Try to retrieve from Redis cache
var cachedData = await _cache.GetStringAsync(cacheKey);
if (cachedData is not null)
{
_logger.LogDebug("Cache hit for {CacheKey}", cacheKey);
var cachedResponse = req.CreateResponse(System.Net.HttpStatusCode.OK);
cachedResponse.Headers.Add("X-Cache", "HIT");
await cachedResponse.WriteStringAsync(cachedData);
return cachedResponse;
}
// Cache miss: load from database
_logger.LogDebug("Cache miss for {CacheKey}, loading from DB", cacheKey);
var products = await _productRepository.GetAllAsync();
var json = System.Text.Json.JsonSerializer.Serialize(products);
// Store in cache for 5 minutes
await _cache.SetStringAsync(cacheKey, json, new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5),
SlidingExpiration = TimeSpan.FromMinutes(2)
});
var response = req.CreateResponse(System.Net.HttpStatusCode.OK);
response.Headers.Add("X-Cache", "MISS");
await response.WriteStringAsync(json);
return response;
}
}
Module 5 – Monitoring and Observability {#module-5}
Application Insights: Optimal Configuration
// Program.cs - Complete Application Insights configuration
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureServices(services =>
{
services.AddApplicationInsightsTelemetryWorkerService();
services.ConfigureFunctionsApplicationInsights();
// Configure sampling to reduce costs
services.Configure<TelemetryConfiguration>(config =>
{
config.DefaultTelemetrySink.TelemetryProcessorChainBuilder
.UseAdaptiveSampling(maxTelemetryItemsPerSecond: 5)
.Build();
});
})
.Build();
// host.json - Logging and sampling configuration
{
"version": "2.0",
"logging": {
"logLevel": {
"default": "Warning",
"ShopTicket.Functions": "Information",
"Host.Results": "Error",
"Function": "Error"
},
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"maxTelemetryItemsPerSecond": 20,
"excludedTypes": "Request;Exception"
},
"enableLiveTelemetry": true
}
}
}
Structured Logging – Best Practices
// ✅ Structured logging with rich context
_logger.LogInformation(
"Order {OrderId} for customer {CustomerId} processed in {DurationMs}ms. Total: {Amount:C}",
order.OrderId,
order.CustomerId,
stopwatch.ElapsedMilliseconds,
order.TotalAmount);
// ✅ Scopes for log correlation
using (_logger.BeginScope(new Dictionary<string, object>
{
["OrderId"] = order.OrderId,
["CustomerId"] = order.CustomerId,
["CorrelationId"] = req.Headers.TryGetValues("X-Correlation-ID", out var vals)
? vals.First()
: Guid.NewGuid().ToString()
}))
{
// All logs in this block will have OrderId and CustomerId properties
_logger.LogDebug("Inventory validation started");
await CheckInventoryAsync(order);
_logger.LogDebug("Inventory validation completed");
}
Distributed Tracing: Tracing a Complete Flow
Azure Functions automatically integrates distributed tracing via Application Insights. Each invocation creates an operation that can be correlated across multiple functions and services.
sequenceDiagram
participant U as User
participant AF1 as HTTP Function\n[operationId: abc-123]
participant Q as Queue
participant AF2 as Queue Function\n[operationId: abc-123]
participant DB as Cosmos DB\n[dependencyId: xyz-456]
U->>AF1: POST /api/orders
AF1->>Q: SendMessage\n[parentId: req-789]
AF1-->>U: 202 Accepted
Q->>AF2: TriggerFunction\n[operationId: abc-123]
AF2->>DB: Upsert document\n[dependencyId: xyz-456]
DB-->>AF2: Success
KQL queries for investigation:
// Trace a complete order processing flow
let orderId = "12345-abcde";
union requests, dependencies, traces, exceptions
| where timestamp > ago(24h)
| where customDimensions["OrderId"] == orderId
or customDimensions["prop__OrderId"] == orderId
| project timestamp,
itemType,
name,
duration,
success,
resultCode,
message,
operation_Id,
operation_ParentId
| order by timestamp asc
// Global health dashboard
requests
| where timestamp > ago(1h)
| summarize
TotalCalls = count(),
SuccessRate = round(countif(success == true) * 100.0 / count(), 1),
AvgDuration = round(avg(duration)),
P95Duration = round(percentile(duration, 95))
by bin(timestamp, 5m), name
| render timechart
// Top 10 errors
exceptions
| where timestamp > ago(24h)
| summarize Count = count() by type, outerMessage
| top 10 by Count desc
Module 6 – Security and Governance {#module-6}
Securing HTTP Endpoints
flowchart LR
Internet --> APIM[Azure API Management\n- Rate limiting\n- OAuth2 validation\n- IP filtering]
APIM --> AF[Azure Functions\n- Function Keys\n- HTTPS only\n- Input validation]
AF --> DB["(Databases\n- RBAC\n- Private Endpoint\n- TLS)"]
KV[Azure Key Vault] -.->|Secrets| AF
AAD[Microsoft Entra ID] -.->|OAuth2 tokens| APIM
style APIM fill:#0078D4,color:#fff
style AF fill:#68217A,color:#fff
style KV fill:#FFB900,color:#000
// Validating Entra ID tokens in an HTTP Function
using Microsoft.Identity.Web;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
namespace ShopTicket.Functions;
public class SecuredOrdersFunction
{
private readonly ILogger<SecuredOrdersFunction> _logger;
public SecuredOrdersFunction(ILogger<SecuredOrdersFunction> logger)
{
_logger = logger;
}
[Function("GetMyOrders")]
public async Task<HttpResponseData> GetMyOrders(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "my/orders")]
HttpRequestData req)
{
// Validate the JWT Bearer token
var authHeader = req.Headers.TryGetValues("Authorization", out var values)
? values.FirstOrDefault()
: null;
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer "))
{
var unauthorized = req.CreateResponse(System.Net.HttpStatusCode.Unauthorized);
await unauthorized.WriteStringAsync("Authentication token required");
return unauthorized;
}
// Actual token validation is done via MSAL middleware
// or via Azure API Management before reaching the Function
var userId = req.FunctionContext.Items["UserId"] as string;
if (userId is null)
{
var forbidden = req.CreateResponse(System.Net.HttpStatusCode.Forbidden);
return forbidden;
}
// Business logic...
var response = req.CreateResponse(System.Net.HttpStatusCode.OK);
await response.WriteAsJsonAsync(new { userId, orders = new[] { "order1", "order2" } });
return response;
}
}
Managed Identities: Complete Configuration
# Managed Identity configuration for access to all services
FUNCTION_APP="func-shopticket-prod"
RG="rg-shopticket-prod"
# 1. Get the Principal ID
PRINCIPAL_ID=$(az functionapp identity show \
--name $FUNCTION_APP \
--resource-group $RG \
--query principalId -o tsv)
# 2. Service Bus - read/write permissions
SB_RESOURCE_ID=$(az servicebus namespace show \
--name sb-shopticket \
--resource-group $RG \
--query id -o tsv)
az role assignment create \
--assignee $PRINCIPAL_ID \
--role "Azure Service Bus Data Owner" \
--scope $SB_RESOURCE_ID
# 3. Cosmos DB - data access
COSMOS_RESOURCE_ID=$(az cosmosdb show \
--name cosmos-shopticket \
--resource-group $RG \
--query id -o tsv)
az cosmosdb sql role assignment create \
--account-name cosmos-shopticket \
--resource-group $RG \
--role-definition-id "00000000-0000-0000-0000-000000000002" \
--principal-id $PRINCIPAL_ID \
--scope $COSMOS_RESOURCE_ID
# 4. Key Vault - secret access
az keyvault set-policy \
--name kv-shopticket \
--object-id $PRINCIPAL_ID \
--secret-permissions get list
# 5. App Settings without secrets (identity-based connections)
az functionapp config appsettings set \
--name $FUNCTION_APP \
--resource-group $RG \
--settings \
"ServiceBusConnection__fullyQualifiedNamespace=sb-shopticket.servicebus.windows.net" \
"CosmosDBConnection__accountEndpoint=https://cosmos-shopticket.documents.azure.com:443/" \
"BlobStorageConnection__serviceUri=https://stshopticket.blob.core.windows.net"
OWASP Top 10: Protection in Azure Functions
| OWASP Risk | Protection Measure | Azure Functions Implementation |
|---|---|---|
| A01: Broken Access Control | RBAC, token validation | Managed Identities + Entra ID OAuth2 |
| A02: Cryptographic Failures | Mandatory HTTPS, TLS 1.2+ | httpsOnly: true, minTlsVersion: '1.2' |
| A03: Injection | Input validation, parameterized queries | Input validation, EF Core parameterized queries |
| A05: Security Misconfiguration | IaC, verified config | Bicep templates + Azure Policy |
| A06: Vulnerable Components | Up-to-date dependencies | Dependabot, .NET security patches |
| A07: Authentication Failures | No anonymous unless required | AuthorizationLevel.Function minimum |
| A09: Security Logging | Complete audit logs | Application Insights + Log Analytics |
Module 7 – Advanced Design Patterns {#module-7}
Saga Pattern: Distributed Transactions
/// <summary>
/// Saga pattern with Durable Functions.
/// Coordinates a distributed transaction across multiple services
/// with compensation on failure.
/// </summary>
[Function(nameof(OrderSagaOrchestration))]
public async Task RunSaga(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var order = context.GetInput<OrderRequest>()!;
var logger = context.CreateReplaySafeLogger(nameof(OrderSagaOrchestration));
var compensations = new Stack<Func<Task>>();
try
{
// Step 1: Reserve inventory
await context.CallActivityAsync(nameof(ReserveInventory), order);
compensations.Push(async () => await context.CallActivityAsync(nameof(ReleaseInventory), order));
logger.LogInformation("Inventory reserved for order {OrderId}", order.OrderId);
// Step 2: Charge payment
var paymentResult = await context.CallActivityAsync<PaymentResult>(nameof(ProcessPayment), order);
compensations.Push(async () => await context.CallActivityAsync(nameof(RefundPayment), paymentResult));
logger.LogInformation("Payment processed for order {OrderId}", order.OrderId);
// Step 3: Generate tickets
await context.CallActivityAsync(nameof(GenerateTickets), order);
logger.LogInformation("Tickets generated for order {OrderId}", order.OrderId);
// Step 4: Send confirmation
await context.CallActivityAsync(nameof(SendConfirmation), order);
logger.LogInformation("Saga completed successfully for order {OrderId}", order.OrderId);
}
catch (Exception ex)
{
logger.LogError(ex, "Saga failed for order {OrderId}, starting compensation", order.OrderId);
// Execute compensations in reverse order
while (compensations.Count > 0)
{
var compensate = compensations.Pop();
try
{
await compensate();
}
catch (Exception compensationEx)
{
logger.LogCritical(compensationEx,
"CRITICAL ERROR: Compensation failed for order {OrderId}", order.OrderId);
// Alert the ops team immediately
}
}
throw;
}
}
CQRS Pattern with Azure Functions
classDiagram
class CommandBus {
+SendAsync(ICommand command)
}
class QueryBus {
+QueryAsync(IQuery query)
}
class WriteModel {
<<Azure Functions - Command Side>>
+CreateOrderFunction
+UpdateOrderFunction
+CancelOrderFunction
}
class ReadModel {
<<Azure Functions - Query Side>>
+GetOrderFunction
+ListOrdersFunction
+GetOrderStatisticsFunction
}
class WriteDatabase {
<<Azure SQL - Normalized>>
+Orders
+OrderItems
+Customers
}
class ReadDatabase {
<<Cosmos DB - Denormalized>>
+OrderSummaries
+CustomerOrders
}
class EventBus {
<<Azure Service Bus>>
+OrderCreated
+OrderUpdated
}
CommandBus --> WriteModel
QueryBus --> ReadModel
WriteModel --> WriteDatabase
WriteModel --> EventBus
EventBus --> ReadModel
ReadModel --> ReadDatabase
Module 8 – Complete Reference Architecture {#module-8}
flowchart TB
subgraph "Public Zone"
Web[SPA React/Blazor\nAzure Static Web Apps]
Mobile[Mobile Application]
Partner[Partner API]
end
subgraph "Secure Zone - API Gateway"
APIM[Azure API Management\nAuth, Rate Limit, Routing]
AFD[Azure Front Door\nGlobal Load Balancing CDN]
end
subgraph "Azure Functions - East Cluster"
direction TB
FA_HTTP[func-api-eastus\nHTTP Triggers]
FA_PROC[func-processing-eastus\nQueue/Service Bus Triggers]
FA_ORCH[func-orchestration-eastus\nDurable Functions]
FA_SCHED[func-scheduler-eastus\nTimer Triggers]
end
subgraph "Messaging"
SB[Azure Service Bus\nTopics & Queues]
EG[Azure Event Grid\nEvent Routing]
EH[Azure Event Hubs\nIoT/Telemetry Streaming]
end
subgraph "Persistence"
SQL["(Azure SQL\nTransactional OLTP)"]
COSMOS["(Cosmos DB\nGeo-replicated)"]
BLOB[Azure Blob Storage\nFiles & Media]
REDIS[Azure Redis Cache\nDistributed Cache]
end
subgraph "Security & Monitoring"
KV[Key Vault\nSecrets]
AAD[Microsoft Entra ID\nIdentities]
AI[Application Insights\nTelemetry]
LA[Log Analytics\nCentralized Logs]
AM[Azure Monitor\nAlerts & Dashboards]
end
Web & Mobile --> AFD
Partner --> APIM
AFD --> APIM
APIM --> FA_HTTP
FA_HTTP --> SB
FA_HTTP --> SQL
FA_HTTP --> REDIS
SB --> FA_PROC
SB --> FA_ORCH
FA_PROC --> COSMOS
FA_PROC --> BLOB
FA_SCHED --> SQL
EH --> FA_PROC
EG --> FA_HTTP
KV -.->|secrets| FA_HTTP & FA_PROC & FA_ORCH
AAD -.->|identity| FA_HTTP & APIM
FA_HTTP & FA_PROC & FA_ORCH & FA_SCHED --> AI
AI --> LA
LA --> AM
style APIM fill:#0078D4,color:#fff
style FA_HTTP fill:#68217A,color:#fff
style FA_PROC fill:#68217A,color:#fff
style FA_ORCH fill:#68217A,color:#fff
style FA_SCHED fill:#68217A,color:#fff
Module 9 – Best Practices and Pitfalls to Avoid {#module-9}
Architectural Review Checklist
flowchart TD
A[New Azure Function] --> B{HTTP Trigger?}
B -->|Yes| C[✅ AuthorizationLevel.Function minimum\n✅ Validate inputs\n✅ HTTPS only\n✅ CORS configured]
B -->|No| D{Queue/Service Bus?}
D -->|Yes| E[✅ Idempotency implemented\n✅ Dead-letter queue monitored\n✅ Retry policies configured\n✅ Timeout adapted to plan]
D -->|No| F{Timer Trigger?}
F -->|Yes| G[✅ Singleton check if multi-instance\n✅ IsPastDue checked\n✅ Timeout > estimated task duration]
C & E & G --> H[For all functions]
H --> I[✅ Structured logging\n✅ Application Insights configured\n✅ Managed Identity vs connection strings\n✅ Key Vault for secrets\n✅ IaC for infrastructure\n✅ Unit and integration tests]
Top 10 Common Pitfalls
| # | Pitfall | Impact | Solution |
|---|---|---|---|
| 1 | HttpClient not reused | Socket exhaustion, 503 errors | Use IHttpClientFactory |
| 2 | Durable orchestrator violations | Non-deterministic behavior, hard-to-debug issues | Follow determinism rules |
| 3 | Non-idempotent queue functions | Duplicates, corrupted data | Check state before acting |
| 4 | Secrets in App Settings | Security leak | Key Vault References |
| 5 | AuthorizationLevel.Anonymous in prod | Unauthorized access | Always use Function or higher |
| 6 | No poison queue monitoring | Silently lost messages | Alerts on poison queue size |
| 7 | Timeout too short on Consumption | Interrupted functions | Check timeout vs actual duration |
| 8 | No retry logic | Transient failures unhandled | Polly + Azure SDK built-in retry |
| 9 | All functions in one App | Globally coupled scaling | Separate by domain and scaling profile |
| 10 | Cold starts ignored | SLA not met | Premium plan or pre-warming |
Glossary {#glossary}
| Term | Definition |
|---|---|
| Active-Active | Multi-region deployment where all regions handle traffic simultaneously |
| Active-Passive | Multi-region deployment where the secondary region is on standby (failover) |
| APIM | Azure API Management – API gateway handling authentication, rate limiting, routing |
| Azure Front Door | Azure’s global CDN and load balancer for HTTP applications |
| Change Feed | Real-time stream of changes in Cosmos DB (inserts and updates) |
| Circuit Breaker | Polly pattern: cuts calls to a temporarily failing service |
| Cold Start | Startup delay for a new Function instance after a period of inactivity |
| CQRS | Command Query Responsibility Segregation – separating reads from writes |
| Dead-letter Queue | Queue for messages that could not be processed after N attempts |
| Distributed Tracing | Correlation of logs and traces across multiple services/functions |
| Event Grid | Azure pub/sub event routing service |
| Event Hub | Azure high-performance event streaming service |
| Fan-out/Fan-in | Pattern of parallelization then aggregation of results |
| Flex Consumption | Hybrid hosting plan between Consumption and Premium |
| Geo-Replication | Automatic data replication across multiple Azure regions |
| Idempotency | Property: running the same operation N times = same result as running it once |
| Jitter | Random variation added to retry delays to avoid request storms |
| Managed Identity | Azure AD identity automatically managed for an Azure resource |
| Pub/Sub | Publish-Subscribe: decoupled messaging pattern |
| RBAC | Role-Based Access Control – Azure role-based access control |
| Request Unit (RU) | Unit of throughput measurement in Azure Cosmos DB |
| Saga | Distributed transaction pattern with compensation on failure |
| Service Bus | Azure enterprise messaging service with sessions, dead-letter, transactions |
| Shard | Partition of a distributed database (Cosmos DB) |
| Stateless | Without state: each invocation is independent of previous ones |
| Task Hub | Durable Functions storage space for orchestration state |
Official documentation: learn.microsoft.com/azure/azure-functions/functions-best-practices.
Search Terms
designing · serverless · architectures · azure · functions · microsoft · integration · configuration · pattern · architecture · patterns · application · availability · concurrency · distributed · dynamic · event · event-driven · high · hosting · http · load · pitfalls · plan