Azure Functions is easy to demo and easy to misuse. The demos show an HTTP trigger returning hello. Production shows orchestrations that span hours, queue triggered processors that must not lose messages, and cold start complaints from whoever chose the hosting plan by accident. Let us do it properly.
Hosting Plans in One Paragraph Each
Flex Consumption is the new default for serverless: scale to zero, per second billing, VNet integration, and configurable always ready instances that eliminate cold starts for your hot paths. Classic Consumption still exists but Flex supersedes it for almost every case. Premium (Elastic Premium) gives you pre warmed instances, longer executions, and bigger SKUs, the choice for latency sensitive APIs with steady traffic. Dedicated App Service plan makes sense only when you have spare capacity on an existing plan. And Functions on Azure Container Apps fits teams already standardized on containers. My decision rule: start Flex, move to Premium when p99 latency or execution duration demands it.
Durable Functions: State Machines Without the State Server
Plain functions are stateless and short lived. Durable Functions adds orchestrations: code that describes a long running workflow, checkpointed automatically to storage, surviving restarts, deployments, and weekends. The orchestrator function replays deterministically from history, which is why orchestrator code must be deterministic: no direct I/O, no DateTime.Now, no random, all of that lives in activity functions.
[Function("OrderOrchestrator")]
public static async Task<OrderResult> RunOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext ctx)
{
var order = ctx.GetInput<Order>();
var reserved = await ctx.CallActivityAsync<bool>(
"ReserveInventory", order);
if (!reserved)
return OrderResult.Rejected("no stock");
try
{
var payment = await ctx.CallActivityAsync<PaymentReceipt>(
"ChargePayment", order,
TaskOptions.FromRetryPolicy(new RetryPolicy(
maxNumberOfAttempts: 3,
firstRetryInterval: TimeSpan.FromSeconds(5),
backoffCoefficient: 2.0)));
await ctx.CallActivityAsync("ConfirmOrder",
new { order.Id, payment.TransactionId });
return OrderResult.Confirmed(payment.TransactionId);
}
catch (TaskFailedException)
{
await ctx.CallActivityAsync("ReleaseInventory", order);
return OrderResult.Failed("payment failed");
}
}
That is a saga with compensation in thirty lines: retries declared, state persisted, rollback explicit. The other patterns you get nearly free: fan out fan in (start N activities, await Task.WhenAll for parallel batch processing), human interaction (WaitForExternalEvent with a durable timer racing an approval), eternal orchestrations via ContinueAsNew for periodic work with state, and sub orchestrations for composing large workflows.
Reliability Rules for Event Driven Functions
For queue and Service Bus triggered functions, the same contract from yesterday applies: PeekLock semantics under the hood, redelivery after failure, so handlers must be idempotent. Let the platform retry and dead letter rather than swallowing exceptions; a caught and logged exception is a completed message as far as Service Bus is concerned, which means silent loss. Keep functions single purpose so the trigger settings (batch size, prefetch, max concurrency) can be tuned per workload, and cap concurrency against downstream capacity, because Functions will happily scale to hundreds of instances and turn your database into the bottleneck. Finally, wire everything to Application Insights with sampling configured, and use the built in distributed tracing so a message’s journey from HTTP front end through queue to processor reads as one trace.
Deployment note: run from package, managed identity for every binding connection (no connection strings with keys), and slot or blue green deployments on Premium. Serverless does not mean configuration free, it means the configuration is all you have.
Cheers
Osama
Leave a comment