| 1 | Overview | 2 | Architecture Overview |
| 3 | Azure Service Topology | 4 | Implementation Guide |
| 5 | Decision Criteria | 6 | Cost Model |
| 7 | Anti-Patterns to Avoid | 8 | References |
Overview
The serverless pattern on Azure inverts the traditional deployment model. Instead of running servers continuously, you deploy functions or containers and Azure runs them only when triggered. Functions on the Consumption plan cost nothing when idle and scale horizontally for concurrent requests. The Premium plan adds VNet integration, no cold starts, and execution up to 60 minutes — necessary for user-facing APIs where a 1–4 second cold start on .NET or Node.js (worse on Java) is unacceptable.
The key architectural decision in Azure serverless is choosing between Azure Functions for single-operation event handlers, Azure Container Apps for workloads that exceed Function runtime constraints (execution time, memory, custom runtimes), and Durable Functions for stateful workflows requiring orchestrator fan-out, fan-in, and async HTTP polling patterns.
Architecture Overview
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','fontFamily':'Inter, system-ui, sans-serif','primaryColor':'#DBEAFE','primaryTextColor':'#1e3a5f','primaryBorderColor':'#2563EB','lineColor':'#374151','clusterBkg':'#F9FAFB','clusterBorder':'#D1D5DB','edgeLabelBackground':'#FFFFFF'},'flowchart':{'curve':'orthogonal','padding':30,'nodeSpacing':65,'rankSpacing':75,'useMaxWidth':true}}}%% flowchart TD START([Client Request or Event Trigger]) START --> TRIGGER{Trigger Type} TRIGGER -->|HTTP request| APIM[API Management\nRate limit, auth, caching\nRouting and versioning] TRIGGER -->|Schedule| TIMER[Timer Trigger\nCron expression\nRetry with poison queue] TRIGGER -->|Queue message| SB_T[Service Bus Queue\nBatch processing\nDead letter queue] TRIGGER -->|Multi-step workflow| DURABLE[Durable Functions\nOrchestrator pattern\nFan-out fan-in, async HTTP] APIM --> FUNC[Azure Functions\nStateless execution\nConsumption or Premium plan] TIMER --> FUNC SB_T --> FUNC DURABLE --> FUNC FUNC --> DECISION{Response Type} DECISION -->|Synchronous| RESP[Return response\nvia API Management] DECISION -->|Async continuation| NEXT[Publish message\nto Service Bus] DECISION -->|Workflow step| DURABLE RESP --> END([Response Returned to Client]) NEXT --> END style START fill:#4f8ef7,color:#fff style END fill:#10b981,color:#fff style DURABLE fill:#e0f2fe style FUNC fill:#e0f2fe
Azure Service Topology
%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','fontFamily':'Inter, system-ui, sans-serif','primaryColor':'#DBEAFE','primaryTextColor':'#1e3a5f','primaryBorderColor':'#2563EB','lineColor':'#374151','clusterBkg':'#F9FAFB','clusterBorder':'#D1D5DB','edgeLabelBackground':'#FFFFFF'},'flowchart':{'curve':'orthogonal','padding':30,'nodeSpacing':65,'rankSpacing':75,'useMaxWidth':true}}}%% flowchart TD CLIENT[Client Application\nWeb, Mobile, or Service] subgraph GATEWAY["Azure API Management — front door for all backends"] AUTH[Policy: JWT Validation\nOAuth 2.0 / AAD\nPer-client rate limiting] CACHE[Response Caching\nBuilt-in Redis cache\nVary by header or query] ROUTE[Backend Routing\nVersioned APIs\nCanary release support] end subgraph FUNCTIONS["Azure Functions — Premium Plan EP1"] FN_API[HTTP-Triggered Functions\ndotnet-isolated runtime\nManaged Identity auth] FN_DURABLE[Durable Functions\nOrchestrator + Activity\nFan-out fan-in pattern] FN_SB[Service Bus Triggered\nBatch dequeue\nPoison message handling] end subgraph CONTAINERS["Azure Container Apps"] CA_APP[Container App\nKEDA autoscaling\nminReplicas 1, maxReplicas 50] CA_ENV[Managed Environment\nLog Analytics workspace\nZone redundant] end subgraph BACKEND["Backend Services"] SB_BE[Service Bus\nQueues and topics\nAt-least-once delivery] KV[Key Vault\nSecrets and certificates\nManaged Identity access] COSMOS[Cosmos DB\nNoSQL, multi-region\nServerless or autoscale RU] ACR[Container Registry\nPrivate image store\nGeo-replication] end subgraph OBS["Observability"] AI[Application Insights\nDistributed tracing\nLive metrics stream] LA[Log Analytics\nKQL queries\nAlerting and dashboards] end CLIENT --> AUTH --> CACHE --> ROUTE ROUTE --> FN_API & CA_APP FN_API --> FN_DURABLE & FN_SB CA_APP --> CA_ENV FN_API --> SB_BE & KV & COSMOS CA_APP --> SB_BE & KV & COSMOS & ACR FN_API --> AI CA_APP --> AI AI --> LA style CLIENT fill:#4f8ef7,color:#fff style FN_DURABLE fill:#e0f2fe style CA_ENV fill:#e0f2fe style KV fill:#fef3c7
Implementation Guide
Bicep — Function App on Premium Plan
The Premium plan (EP1/ElasticPremium) eliminates cold starts, enables VNet integration, and allows execution up to 60 minutes. Managed Identity replaces all connection strings.
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' existing = {
name: storageAccountName
}
resource hostingPlan 'Microsoft.Web/serverfarms@2022-09-01' = {
name: '${prefix}-plan'
location: location
sku: {
name: 'EP1'
tier: 'ElasticPremium'
}
kind: 'elastic'
properties: {
maximumElasticWorkerCount: 20
reserved: false
}
}
resource functionApp 'Microsoft.Web/sites@2022-09-01' = {
name: '${prefix}-func'
location: location
kind: 'functionapp'
identity: {
type: 'SystemAssigned'
}
properties: {
serverFarmId: hostingPlan.id
httpsOnly: true
siteConfig: {
minTlsVersion: '1.2'
netFrameworkVersion: 'v8.0'
appSettings: [
{
name: 'FUNCTIONS_EXTENSION_VERSION'
value: '~4'
}
{
name: 'FUNCTIONS_WORKER_RUNTIME'
value: 'dotnet-isolated'
}
{
// Managed Identity auth — no storage key in config
name: 'AzureWebJobsStorage__accountName'
value: storageAccount.name
}
{
// Managed Identity auth — no Service Bus connection string
name: 'ServiceBusConnection__fullyQualifiedNamespace'
value: '${serviceBusNamespaceName}.servicebus.windows.net'
}
{
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
value: appInsights.properties.ConnectionString
}
]
}
}
}
// Grant Function App identity access to Service Bus
resource sbRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(serviceBusNamespace.id, functionApp.id, sbDataReceiverRoleId)
scope: serviceBusNamespace
properties: {
roleDefinitionId: subscriptionResourceId(
'Microsoft.Authorization/roleDefinitions', sbDataReceiverRoleId)
principalId: functionApp.identity.principalId
principalType: 'ServicePrincipal'
}
}
Terraform equivalent: Use
azurerm_linux_function_appwithidentity { type = "SystemAssigned" }. Setapp_settingswithAzureWebJobsStorage__accountNameandServiceBusConnection__fullyQualifiedNamespace. Assignazurerm_role_assignmentforAzure Service Bus Data Receiverscoped to the namespace.
Bicep — Container Apps Environment and App
Container Apps provide managed Kubernetes with KEDA scaling. Internal ingress keeps traffic off the public internet; API Management is the sole external entry point.
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = {
name: logAnalyticsWorkspaceName
}
resource managedEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' = {
name: '${prefix}-cae'
location: location
properties: {
appLogsConfiguration: {
destination: 'log-analytics'
logAnalyticsConfiguration: {
customerId: logAnalytics.properties.customerId
sharedKey: logAnalytics.listKeys().primarySharedKey
}
}
zoneRedundant: true
}
}
resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
name: '${prefix}-ca'
location: location
identity: {
type: 'SystemAssigned'
}
properties: {
managedEnvironmentId: managedEnvironment.id
configuration: {
ingress: {
external: false // internal only — APIM is front door
targetPort: 8080
transport: 'http2'
allowInsecure: false
}
registries: [
{
server: containerRegistry.properties.loginServer
identity: 'system' // Managed Identity pull from ACR
}
]
}
template: {
containers: [
{
name: 'api'
image: '${containerRegistry.properties.loginServer}/api:${imageTag}'
resources: {
cpu: json('0.5')
memory: '1Gi'
}
env: [
{
name: 'COSMOS_ENDPOINT'
value: cosmosAccount.properties.documentEndpoint
}
]
}
]
scale: {
minReplicas: 1 // avoid cold start for user-facing traffic
maxReplicas: 50
rules: [
{
name: 'http-scaling'
http: {
metadata: {
concurrentRequests: '50'
}
}
}
]
}
}
}
}
Terraform equivalent: Use
azurerm_container_app_environmentwithlog_analytics_workspace_idandzone_redundancy_enabled = true. Useazurerm_container_appwithidentity { type = "SystemAssigned" }andingress { external_enabled = false, transport = "http2" }. Setmin_replicas = 1inside thetemplateblock.
Decision Criteria
| Decision point | Azure Functions | Container Apps |
|---|---|---|
| Execution duration | Up to 10 min (Consumption), 60 min (Premium) | Unlimited (container process lifetime) |
| Runtime flexibility | .NET, Node.js, Python, Java, PowerShell | Any language, any base image |
| Scaling trigger | HTTP, queue, blob, timer, Event Hub | HTTP, KEDA (queue depth, CPU, cron, custom) |
| Cold start sensitivity | Consumption: 1–4 s (.NET/Node), worse Java; Premium: none | minReplicas 1 = no cold start; minReplicas 0 = cold start on first request |
| State and orchestration | Durable Functions (built-in) | External workflow (Dapr, Service Bus choreography) |
| VNet integration | Premium plan or Flex Consumption | Always VNet-capable via managed environment |
| Operations overhead | Near-zero; Azure manages runtime | Low; managed Kubernetes, no node management |
| Cost at low traffic | Consumption plan scales to zero | Scales to zero only with minReplicas 0; EP1 ~$150/month always-on |
Cost Model
| Service | Free / included | Pay-as-you-go |
|---|---|---|
| Azure Functions (Consumption) | 1M executions/month, 400K GB-seconds | $0.20/million executions + $0.000016/GB-second |
| Azure Functions (Premium EP1) | Included vCPU and memory | ~$0.173/vCPU-hour; approximately $125–150/month baseline |
| Container Apps | 180K vCPU-seconds, 360K GiB-seconds free/month | $0.000024/vCPU-second + $0.000003/GiB-second |
| API Management (Consumption tier) | 1M calls/month | $3.50/million calls |
| Service Bus (Standard) | 10M operations/month | $0.10/million operations |
| Cosmos DB (Serverless) | None | $0.25/million RUs + $0.25/GB |
A typical serverless API handling 10M requests per month at 200 ms average duration on Azure Functions Consumption costs approximately $10–18 per month including Service Bus and Cosmos DB serverless.
Cost optimisation levers:
- Use Consumption plan for background jobs and low-traffic APIs; reserve Premium plan for user-facing endpoints where cold start is a customer-visible latency regression.
- Set Container Apps
minReplicas: 0for non-interactive workloads (batch processors, async handlers) andminReplicas: 1for synchronous API paths. - Use Cosmos DB Serverless for development and unpredictable-traffic workloads; switch to autoscale RU at sustained load above ~1B RUs/month.
- Route inter-service traffic through the managed environment VNet rather than the public internet to avoid egress charges.
- Consolidate Log Analytics workspaces; per-GB ingestion at $2.30/GB makes log verbosity a direct cost lever.
Anti-Patterns to Avoid
Placing compute-heavy or long-duration operations (report generation, video transcoding, large ETL batches) on Consumption plan Functions hits the 10-minute execution limit, triggers forced termination, and leaves work in an inconsistent state with no native compensation.
Use Durable Functions for workflows that can be checkpointed across activity steps. For jobs exceeding 60 minutes or requiring custom runtimes, use Container Apps or Azure Container Instances triggered by a Service Bus message.
Storing database connection strings, API keys, or Service Bus connection strings as plain appSettings values. These appear in the Azure Portal in plain text, are captured in ARM export artefacts, and violate least-privilege access control.
Use Managed Identity everywhere. Replace AzureWebJobsStorage with AzureWebJobsStorage__accountName (identity-based), replace Service Bus connection strings with ServiceBusConnection__fullyQualifiedNamespace, and reference Key Vault secrets via @Microsoft.KeyVault(SecretUri=...) for any third-party credentials that cannot use identity.
Setting minReplicas: 0 on Container Apps or using Consumption plan Functions for APIs that serve end users directly. The first request after an idle period incurs a cold start of 1–4 seconds on .NET and Node.js, measurably longer on Java, degrading perceived performance and increasing error rates under traffic spikes.
Set minReplicas: 1 on Container Apps serving interactive traffic. For Functions, use the Premium plan on user-facing paths and reserve Consumption for background triggers where latency is not customer-visible.
Using Logic Apps visual workflows to encode branching business logic, data transformation, or computation. Logic Apps connectors add per-execution cost at scale, the visual editor becomes unmanageable past 20–30 steps, and business logic cannot be unit-tested or version-diffed meaningfully.
Extract business logic to Azure Functions or Container Apps. Use Logic Apps only for integration plumbing — connecting SaaS systems, triggering on external events, and light data mapping — where its 400+ managed connectors provide genuine value over custom code.
Flowchart
References
- Microsoft — Azure Functions hosting plans. https://learn.microsoft.com/azure/azure-functions/functions-scale
- Microsoft — Durable Functions overview. https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview
- Microsoft — Scale a container app in Azure Container Apps. https://learn.microsoft.com/azure/container-apps/scale-app
- Microsoft — Use managed identity for Azure Functions with Azure SQL. https://learn.microsoft.com/azure/azure-functions/functions-identity-access-azure-sql-with-managed-identity
- Microsoft — Azure API Management documentation. https://learn.microsoft.com/azure/api-management/
- Microsoft — KEDA-based scaling in Container Apps. https://learn.microsoft.com/azure/container-apps/scale-app#scale-triggers
- Microsoft — Azure Well-Architected Framework: serverless. https://learn.microsoft.com/azure/architecture/framework/serverless/
- Portal: AWS serverless comparison