Deep expertise in Azure Functions serverless compute — build HTTP APIs, event-driven processors, and orchestration workflows with triggers, bindings, Durable Functions, and Azure Functions Core Tools. Covers Consumption/Premium/Flex hosting plans, v4 Node.js/TypeScript programming model, cold start optimization, deployment pipelines, and Application Insights monitoring. Targets professional TypeScript developers building production serverless applications.
Resources
1Install
npx skillscat add markus41/claude-m/azure-functions Install via the SkillsCat registry.
Azure Functions
Shared Workflow Routing
- Use the shared workflow spec for deterministic multi-plugin routing: `workflows/multi-plugin-workflows.md`.
- Apply the trigger phrases, handoff contracts, auth prerequisites, validation checkpoints, and stop conditions before escalating to the next plugin.
1. Azure Functions Overview
Azure Functions is a serverless compute service that runs event-driven code without managing infrastructure. Functions scale automatically and you pay only for compute time consumed.
Hosting plans:
| Plan | Scaling | Timeout | vCPU/Memory | Use Case |
|---|---|---|---|---|
| Consumption | Event-driven, 0-200 instances | 5 min (default), max 10 min | 1.5 GB | Low-traffic, sporadic workloads |
| Flex Consumption | Event-driven, 0-1000 instances | 30 min (default) | Configurable | Variable traffic with fast scale |
| Premium (EP1-EP3) | Event-driven, 1-100+ instances | 30 min (default), unlimited config | 3.5-14 GB | Always-warm, VNet, large payloads |
| Dedicated (App Service) | Manual/autoscale | Unlimited | Plan-dependent | Existing App Service plan reuse |
Runtime versions and language support:
| Runtime | Node.js | .NET | Python | Java |
|---|---|---|---|---|
| v4 (current) | 18, 20 | .NET 8 (isolated) | 3.9-3.11 | 11, 17, 21 |
Cold start: Functions on Consumption plan may experience cold starts (typically 1-3 seconds for Node.js). Mitigation strategies:
- Use Premium plan with minimum instance count for always-warm instances.
- Use Flex Consumption with always-ready instance counts.
- Keep function packages small — minimize dependencies.
- Use
WEBSITE_RUN_FROM_PACKAGE=1for faster startup.
v4 programming model (Node.js/TypeScript):
The v4 model uses programmatic registration instead of function.json files. Functions are registered using app.http(), app.timer(), app.storageBlob(), etc. This is the recommended model for all new TypeScript projects.
import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions";
export async function hello(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
return { body: `Hello, ${request.query.get("name") || "world"}!` };
}
app.http("hello", {
methods: ["GET"],
authLevel: "anonymous",
handler: hello,
});2. Azure Functions Core Tools CLI
Azure Functions Core Tools (func) is the local development CLI for creating, running, and deploying functions.
Install:
npm install -g azure-functions-core-tools@4 --unsafe-perm trueCore commands:
| Command | Description |
|---|---|
func init <name> --typescript --model V4 |
Initialize a new TypeScript project with v4 model |
func new --template "HTTP trigger" --name myFunc |
Add a new function from template |
func start |
Start the local Functions runtime |
func azure functionapp publish <app-name> |
Deploy to Azure |
func azure functionapp list-functions <app-name> |
List deployed functions |
func azure functionapp logstream <app-name> |
Stream live logs |
func settings list |
Show local settings |
func settings add <name> <value> |
Add a local setting |
func extensions install |
Install binding extensions |
Project structure (v4 TypeScript model):
my-functions-app/
├── host.json # Runtime configuration
├── local.settings.json # Local environment variables (NOT committed)
├── package.json # Dependencies (@azure/functions v4)
├── tsconfig.json # TypeScript configuration
├── .funcignore # Files to exclude from deployment
├── .gitignore # Git ignore (includes local.settings.json)
├── src/
│ └── functions/
│ ├── httpTrigger1.ts # HTTP-triggered function
│ ├── timerTrigger1.ts # Timer-triggered function
│ └── queueTrigger1.ts # Queue-triggered function
└── dist/ # Compiled JavaScript outputhost.json (runtime configuration):
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
}
}
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.*, 5.0.0)"
},
"functionTimeout": "00:05:00"
}local.settings.json (local development only):
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "node"
}
}3. Triggers
Triggers define how a function is invoked. Each function has exactly one trigger.
Trigger Reference Table
| Trigger | Registration Method | First Parameter | Key Options |
|---|---|---|---|
| HTTP | app.http() |
HttpRequest |
methods, authLevel, route |
| Timer | app.timer() |
Timer |
schedule (CRON), runOnStartup |
| Blob Storage | app.storageBlob() |
Buffer |
path, connection |
| Queue Storage | app.storageQueue() |
unknown |
queueName, connection |
| Service Bus Queue | app.serviceBusQueue() |
unknown |
queueName, connection |
| Service Bus Topic | app.serviceBusTopic() |
unknown |
topicName, subscriptionName, connection |
| Event Grid | app.eventGrid() |
EventGridEvent |
(none required) |
| Event Hub | app.eventHub() |
unknown |
eventHubName, connection, cardinality |
| Cosmos DB | app.cosmosDB() |
unknown[] |
connection, databaseName, containerName, leaseContainerName |
| SignalR | app.generic() with type: "signalRTrigger" |
InvocationContext |
hubName, category, event |
HTTP Trigger
import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions";
export async function httpTrigger(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
context.log(`HTTP ${request.method} ${request.url}`);
const name = request.query.get("name") || (await request.text()) || "world";
return {
status: 200,
headers: { "Content-Type": "application/json" },
jsonBody: { message: `Hello, ${name}!` },
};
}
app.http("httpTrigger", {
methods: ["GET", "POST"],
authLevel: "anonymous",
route: "hello/{name?}",
handler: httpTrigger,
});Timer Trigger (CRON)
CRON expressions use 6 fields: {second} {minute} {hour} {day} {month} {day-of-week}.
| Expression | Schedule |
|---|---|
0 */5 * * * * |
Every 5 minutes |
0 0 * * * * |
Every hour |
0 0 8 * * * |
Daily at 8:00 AM UTC |
0 0 0 * * 1 |
Every Monday at midnight |
0 30 9 * * 1-5 |
Weekdays at 9:30 AM |
import { app, InvocationContext, Timer } from "@azure/functions";
export async function scheduledCleanup(myTimer: Timer, context: InvocationContext): Promise<void> {
context.log("Timer function executed at:", context.df?.currentUtcDateTime ?? new Date().toISOString());
if (myTimer.isPastDue) {
context.log("Timer is running late!");
}
// Perform cleanup logic
}
app.timer("scheduledCleanup", {
schedule: "0 0 2 * * *", // Daily at 2:00 AM UTC
runOnStartup: false, // Do NOT set true in production
handler: scheduledCleanup,
});Blob Storage Trigger
import { app, InvocationContext } from "@azure/functions";
export async function blobProcessor(blob: Buffer, context: InvocationContext): Promise<void> {
const blobName = context.triggerMetadata.name as string;
context.log(`Processing blob "${blobName}", size: ${blob.length} bytes`);
// Process blob content
const content = blob.toString("utf-8");
context.log(`First 100 chars: ${content.substring(0, 100)}`);
}
app.storageBlob("blobProcessor", {
path: "uploads/{name}",
connection: "AzureWebJobsStorage",
handler: blobProcessor,
});Queue Storage Trigger
import { app, InvocationContext } from "@azure/functions";
interface OrderMessage {
orderId: string;
customerId: string;
amount: number;
}
export async function processOrder(queueItem: OrderMessage, context: InvocationContext): Promise<void> {
context.log(`Processing order ${queueItem.orderId} for customer ${queueItem.customerId}`);
context.log(`Amount: $${queueItem.amount}`);
}
app.storageQueue("processOrder", {
queueName: "order-queue",
connection: "AzureWebJobsStorage",
handler: processOrder,
});Service Bus Trigger
import { app, InvocationContext } from "@azure/functions";
export async function serviceBusHandler(message: unknown, context: InvocationContext): Promise<void> {
context.log("Service Bus message received:", message);
context.log("Message ID:", context.triggerMetadata.messageId);
context.log("Delivery count:", context.triggerMetadata.deliveryCount);
}
app.serviceBusQueue("serviceBusHandler", {
queueName: "myqueue",
connection: "ServiceBusConnection",
handler: serviceBusHandler,
});Event Grid Trigger
import { app, EventGridEvent, InvocationContext } from "@azure/functions";
export async function eventGridHandler(event: EventGridEvent, context: InvocationContext): Promise<void> {
context.log("Event Grid event received:");
context.log(" Subject:", event.subject);
context.log(" Event Type:", event.eventType);
context.log(" Data:", JSON.stringify(event.data));
}
app.eventGrid("eventGridHandler", {
handler: eventGridHandler,
});Event Hub Trigger
import { app, InvocationContext } from "@azure/functions";
export async function eventHubHandler(messages: unknown[], context: InvocationContext): Promise<void> {
for (const message of messages) {
context.log("Event Hub message:", message);
}
context.log(`Processed ${messages.length} events`);
}
app.eventHub("eventHubHandler", {
eventHubName: "my-event-hub",
connection: "EventHubConnection",
cardinality: "many",
handler: eventHubHandler,
});Cosmos DB Change Feed Trigger
import { app, InvocationContext } from "@azure/functions";
export async function cosmosDBHandler(documents: unknown[], context: InvocationContext): Promise<void> {
context.log(`Cosmos DB change feed: ${documents.length} documents changed`);
for (const doc of documents) {
context.log("Changed document:", JSON.stringify(doc));
}
}
app.cosmosDB("cosmosDBHandler", {
connection: "CosmosDBConnection",
databaseName: "mydb",
containerName: "items",
leaseContainerName: "leases",
createLeaseContainerIfNotExists: true,
handler: cosmosDBHandler,
});4. Input/Output Bindings
Bindings connect functions to external resources without writing explicit client code. Input bindings read data; output bindings write data.
Binding Types Reference
| Binding | Direction | Registration | Key Options |
|---|---|---|---|
| Blob Storage | Input | input.storageBlob() |
path, connection |
| Blob Storage | Output | output.storageBlob() |
path, connection |
| Table Storage | Input | input.table() |
tableName, partitionKey, rowKey, connection |
| Table Storage | Output | output.table() |
tableName, connection |
| Queue Storage | Output | output.storageQueue() |
queueName, connection |
| Cosmos DB | Input | input.cosmosDB() |
connection, databaseName, containerName, id, partitionKey |
| Cosmos DB | Output | output.cosmosDB() |
connection, databaseName, containerName |
| Service Bus Queue | Output | output.serviceBusQueue() |
queueName, connection |
| Service Bus Topic | Output | output.serviceBusTopic() |
topicName, connection |
| Event Hub | Output | output.eventHub() |
eventHubName, connection |
| SendGrid | Output | output.sendGrid() |
apiKey, from, to, subject |
| SignalR | Output | output.generic({ type: "signalR" }) |
hubName |
| SQL | Input | input.sql() |
commandText, connectionStringSetting |
| SQL | Output | output.sql() |
commandText, connectionStringSetting |
Input Binding Example (Cosmos DB)
import { app, HttpRequest, HttpResponseInit, InvocationContext, input } from "@azure/functions";
const cosmosInput = input.cosmosDB({
connection: "CosmosDBConnection",
databaseName: "mydb",
containerName: "products",
id: "{id}",
partitionKey: "{category}",
});
export async function getProduct(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
const product = context.extraInputs.get(cosmosInput);
if (!product) {
return { status: 404, jsonBody: { error: "Product not found" } };
}
return { jsonBody: product };
}
app.http("getProduct", {
methods: ["GET"],
authLevel: "function",
route: "products/{category}/{id}",
extraInputs: [cosmosInput],
handler: getProduct,
});Output Binding Example (Queue + Table)
import { app, HttpRequest, HttpResponseInit, InvocationContext, output } from "@azure/functions";
const queueOutput = output.storageQueue({
queueName: "order-processing",
connection: "AzureWebJobsStorage",
});
const tableOutput = output.table({
tableName: "OrderLog",
connection: "AzureWebJobsStorage",
});
export async function createOrder(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
const order = await request.json() as { id: string; product: string; quantity: number };
// Write to queue for async processing
context.extraOutputs.set(queueOutput, {
orderId: order.id,
product: order.product,
quantity: order.quantity,
});
// Write to table for audit log
context.extraOutputs.set(tableOutput, {
partitionKey: "orders",
rowKey: order.id,
product: order.product,
quantity: order.quantity,
createdAt: new Date().toISOString(),
});
return { status: 201, jsonBody: { message: "Order created", orderId: order.id } };
}
app.http("createOrder", {
methods: ["POST"],
authLevel: "function",
route: "orders",
extraOutputs: [queueOutput, tableOutput],
handler: createOrder,
});Blob Input/Output Bindings
import { app, HttpRequest, HttpResponseInit, InvocationContext, input, output } from "@azure/functions";
const blobInput = input.storageBlob({
path: "templates/{templateName}.html",
connection: "AzureWebJobsStorage",
});
const blobOutput = output.storageBlob({
path: "rendered/{templateName}-{datetime:yyyy-MM-dd}.html",
connection: "AzureWebJobsStorage",
});
export async function renderTemplate(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
const template = context.extraInputs.get(blobInput) as string;
const data = await request.json() as Record<string, string>;
let rendered = template;
for (const [key, value] of Object.entries(data)) {
rendered = rendered.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
}
context.extraOutputs.set(blobOutput, rendered);
return { body: rendered, headers: { "Content-Type": "text/html" } };
}
app.http("renderTemplate", {
methods: ["POST"],
authLevel: "function",
route: "render/{templateName}",
extraInputs: [blobInput],
extraOutputs: [blobOutput],
handler: renderTemplate,
});5. Durable Functions
Durable Functions extend Azure Functions with stateful orchestrations, enabling complex workflows with automatic checkpointing, replay, and error recovery.
Components:
| Component | Purpose | Registration |
|---|---|---|
| Client function | Starts and manages orchestrations | df.app.client.http() |
| Orchestrator | Defines workflow logic (must be deterministic) | df.app.orchestration() |
| Activity | Performs single units of work (I/O allowed) | df.app.activity() |
| Entity | Manages small pieces of state (actor model) | df.app.entity() |
Install:
npm install durable-functionsFunction Chaining Pattern
Sequential execution where each activity's output feeds the next activity's input.
import * as df from "durable-functions";
import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions";
// Orchestrator
df.app.orchestration("orderProcessingOrchestrator", function* (context: df.OrchestrationContext) {
const order = context.df.getInput() as { orderId: string; items: string[] };
const validated = yield context.df.callActivity("validateOrder", order);
const reserved = yield context.df.callActivity("reserveInventory", validated);
const charged = yield context.df.callActivity("processPayment", reserved);
const confirmation = yield context.df.callActivity("sendConfirmation", charged);
return confirmation;
});
// Activities
df.app.activity("validateOrder", {
handler: async (input: { orderId: string; items: string[] }, context: InvocationContext) => {
context.log(`Validating order ${input.orderId}`);
// Validate items exist, prices correct, etc.
return { ...input, validated: true };
},
});
df.app.activity("reserveInventory", {
handler: async (input: unknown, context: InvocationContext) => {
context.log("Reserving inventory");
return { ...input as object, reserved: true };
},
});
df.app.activity("processPayment", {
handler: async (input: unknown, context: InvocationContext) => {
context.log("Processing payment");
return { ...input as object, paid: true };
},
});
df.app.activity("sendConfirmation", {
handler: async (input: unknown, context: InvocationContext) => {
context.log("Sending confirmation email");
return { ...input as object, confirmed: true };
},
});
// Client starter
df.app.client.http("startOrderProcessing", {
route: "orders/process",
handler: async (request: HttpRequest, client: df.DurableClient, context: InvocationContext) => {
const body = await request.json();
const instanceId = await client.startNew("orderProcessingOrchestrator", { input: body });
context.log(`Started orchestration: ${instanceId}`);
return client.createCheckStatusResponse(request, instanceId);
},
});Fan-Out/Fan-In Pattern
Parallel execution of multiple activities, then aggregate results.
df.app.orchestration("batchProcessorOrchestrator", function* (context: df.OrchestrationContext) {
const items: string[] = yield context.df.callActivity("getWorkItems");
// Fan out — start all tasks in parallel
const tasks = items.map((item) => context.df.callActivity("processWorkItem", item));
// Fan in — wait for all tasks to complete
const results: unknown[] = yield context.df.Task.all(tasks);
// Aggregate results
yield context.df.callActivity("summarizeResults", results);
return { processed: results.length };
});Human Interaction Pattern
Wait for an external event (e.g., human approval) with a timeout.
df.app.orchestration("approvalOrchestrator", function* (context: df.OrchestrationContext) {
const request = context.df.getInput() as { requestId: string; requester: string; amount: number };
// Send approval request notification
yield context.df.callActivity("sendApprovalRequest", request);
// Wait for approval or timeout (24 hours)
const expiration = new Date(context.df.currentUtcDateTime);
expiration.setHours(expiration.getHours() + 24);
const timeoutTask = context.df.createTimer(expiration);
const approvalTask = context.df.waitForExternalEvent("ApprovalResponse");
const winner = yield context.df.Task.any([approvalTask, timeoutTask]);
if (winner === approvalTask) {
timeoutTask.cancel();
const approved = (approvalTask.result as { approved: boolean }).approved;
if (approved) {
yield context.df.callActivity("processApproval", request);
return { status: "approved" };
} else {
yield context.df.callActivity("processRejection", request);
return { status: "rejected" };
}
} else {
yield context.df.callActivity("handleTimeout", request);
return { status: "timed-out" };
}
});Raise an external event (via HTTP):
POST /runtime/webhooks/durabletask/instances/{instanceId}/raiseEvent/ApprovalResponse
Content-Type: application/json
{ "approved": true, "approver": "admin@contoso.com" }Monitor Pattern
Periodic polling with exponential backoff.
df.app.orchestration("monitorOrchestrator", function* (context: df.OrchestrationContext) {
const input = context.df.getInput() as { jobId: string; maxRetries: number };
let pollingInterval = 10000; // Start at 10 seconds
for (let i = 0; i < input.maxRetries; i++) {
const status = yield context.df.callActivity("checkJobStatus", input.jobId);
if (status === "completed") {
yield context.df.callActivity("notifyCompletion", input.jobId);
return { status: "completed", attempts: i + 1 };
}
// Wait with exponential backoff (max 5 minutes)
const nextCheck = new Date(context.df.currentUtcDateTime);
nextCheck.setMilliseconds(nextCheck.getMilliseconds() + pollingInterval);
yield context.df.createTimer(nextCheck);
pollingInterval = Math.min(pollingInterval * 2, 300000);
}
return { status: "max-retries-exceeded" };
});Durable Entity (Virtual Actor)
df.app.entity("Counter", {
handler: (context: df.EntityContext<number>) => {
const state = context.df.getState(() => 0);
switch (context.df.operationName) {
case "add":
context.df.setState(state + (context.df.getInput() as number));
break;
case "reset":
context.df.setState(0);
break;
case "get":
context.df.return(state);
break;
}
},
});Orchestrator Determinism Rules
Orchestrator functions MUST be deterministic — they are replayed from history on each invocation:
| DO NOT use in orchestrators | USE instead |
|---|---|
Date.now(), new Date() |
context.df.currentUtcDateTime |
Math.random() |
context.df.newGuid() |
setTimeout, setInterval |
context.df.createTimer(date) |
| HTTP requests, DB queries | context.df.callActivity("name", input) |
fs.readFile, file I/O |
Activity functions |
| Non-deterministic libraries | Activity functions |
6. HTTP API Patterns
Route Templates
// Simple route
app.http("getUsers", { route: "users", methods: ["GET"], ... });
// Route with parameter
app.http("getUser", { route: "users/{userId}", methods: ["GET"], ... });
// Optional parameter
app.http("getItems", { route: "items/{category?}", methods: ["GET"], ... });
// Constrained parameter
app.http("getOrder", { route: "orders/{orderId:int}", methods: ["GET"], ... });Accessing route parameters:
export async function getUser(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
const userId = request.params.userId;
// ...
}Auth Levels
| Level | Description | Key Required |
|---|---|---|
anonymous |
No authentication required | No |
function |
Function-specific API key in x-functions-key header or code query param |
Yes |
admin |
Host master key required | Yes |
Request/Response Handling
export async function apiHandler(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
// Read query parameters
const page = parseInt(request.query.get("page") || "1");
// Read headers
const authHeader = request.headers.get("Authorization");
// Read JSON body
const body = await request.json() as { name: string };
// Read form data
const formData = await request.formData();
// Read raw body
const rawBody = await request.text();
// Return JSON response
return {
status: 200,
headers: {
"Content-Type": "application/json",
"X-Request-Id": context.invocationId,
},
jsonBody: { message: "Success", data: body },
};
}CORS Configuration
In host.json:
{
"extensions": {
"http": {
"routePrefix": "api",
"customHeaders": {
"Access-Control-Allow-Origin": "https://contoso.com",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE",
"Access-Control-Allow-Headers": "Content-Type, Authorization"
}
}
}
}Or in local.settings.json for local development:
{
"Host": {
"CORS": "http://localhost:3000",
"CORSCredentials": true
}
}7. Deployment
Azure CLI Deployment
# Build the TypeScript project
npm run build
# Deploy using Core Tools
func azure functionapp publish <app-name>
# Deploy with build flag (runs npm install on Azure)
func azure functionapp publish <app-name> --build remote
# Deploy to a specific slot
func azure functionapp publish <app-name> --slot stagingApp Settings Management
# Set app settings
az functionapp config appsettings set \
--name <app-name> --resource-group <rg-name> \
--settings "CosmosDBConnection=AccountEndpoint=..." "ServiceBusConnection=Endpoint=..."
# List current settings
az functionapp config appsettings list \
--name <app-name> --resource-group <rg-name> --output table
# Delete a setting
az functionapp config appsettings delete \
--name <app-name> --resource-group <rg-name> --setting-names "OldSetting"GitHub Actions CI/CD Workflow
name: Deploy Azure Functions
on:
push:
branches: [main]
workflow_dispatch:
env:
AZURE_FUNCTIONAPP_NAME: 'my-func-app'
NODE_VERSION: '18.x'
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Run tests
run: npm test --if-present
- name: Login to Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy to Azure Functions
uses: azure/functions-action@v1
with:
app-name: ${{ env.AZURE_FUNCTIONAPP_NAME }}
package: '.'Deployment Slots
# Create staging slot
az functionapp deployment slot create \
--name <app-name> --resource-group <rg-name> --slot staging
# Deploy to staging
func azure functionapp publish <app-name> --slot staging
# Swap staging to production (zero-downtime)
az functionapp deployment slot swap \
--name <app-name> --resource-group <rg-name> \
--slot staging --target-slot production8. App Settings & Configuration
local.settings.json
Used only for local development. Never commit to source control.
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "node",
"CosmosDBConnection": "AccountEndpoint=https://mydb.documents.azure.com:443/;AccountKey=...",
"ServiceBusConnection": "Endpoint=sb://mysb.servicebus.windows.net/;SharedAccessKeyName=...;SharedAccessKey=...",
"APPINSIGHTS_INSTRUMENTATIONKEY": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
}Key Vault References
Instead of storing secrets directly in app settings, reference Azure Key Vault:
@Microsoft.KeyVault(SecretUri=https://myvault.vault.azure.net/secrets/CosmosDBKey/)
@Microsoft.KeyVault(VaultName=myvault;SecretName=CosmosDBKey;SecretVersion=abc123)Set via CLI:
az functionapp config appsettings set \
--name <app-name> --resource-group <rg-name> \
--settings "CosmosDBConnection=@Microsoft.KeyVault(VaultName=myvault;SecretName=CosmosDBConnection)"Requires the Function App to have a managed identity with Key Vault Secrets User role on the vault.
Managed Identity for Connections
Use identity-based connections instead of connection string secrets:
{
"Values": {
"AzureWebJobsStorage__accountName": "mystorageaccount",
"CosmosDBConnection__accountEndpoint": "https://mydb.documents.azure.com:443/",
"ServiceBusConnection__fullyQualifiedNamespace": "mysb.servicebus.windows.net"
}
}The Function App's managed identity must have the appropriate RBAC roles:
| Service | Required Role |
|---|---|
| Storage (triggers) | Storage Blob Data Owner + Storage Queue Data Contributor + Storage Account Contributor |
| Cosmos DB | Cosmos DB Built-in Data Contributor |
| Service Bus | Azure Service Bus Data Receiver |
| Event Hubs | Azure Event Hubs Data Receiver |
9. Monitoring & Diagnostics
Application Insights Integration
Enable via host.json:
{
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"maxTelemetryItemsPerSecond": 20,
"excludedTypes": "Request"
}
},
"logLevel": {
"default": "Information",
"Host.Results": "Error",
"Function": "Information",
"Host.Aggregator": "Trace"
}
}
}Structured Logging
export async function myFunction(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
context.log("Processing request", { url: request.url, method: request.method });
context.warn("Slow query detected", { duration: 2500 });
context.error("Failed to process", { error: "Connection timeout" });
// Trace with custom properties (appears in Application Insights customDimensions)
context.trace("Custom metric", { orderId: "ORD-123", processingTime: 450 });
return { status: 200 };
}Key Diagnostic Queries (KQL)
Failed function executions:
requests
| where success == false
| where timestamp > ago(24h)
| summarize count() by name, resultCode
| order by count_ descFunction execution duration:
requests
| where timestamp > ago(24h)
| summarize avg(duration), percentile(duration, 95), max(duration) by name
| order by avg_duration descLive Metrics (real-time monitoring):
func azure functionapp logstream <app-name>Custom Metrics
import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions";
export async function trackedFunction(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
const startTime = Date.now();
// Business logic
const result = await processRequest(request);
const duration = Date.now() - startTime;
context.log(`Request processed in ${duration}ms`, {
customMetric: "processingDuration",
value: duration,
operation: "processRequest",
});
return { jsonBody: result };
}10. Authentication
Built-in Authentication (EasyAuth)
Enable via Azure Portal or CLI without code changes:
az webapp auth update --name <app-name> --resource-group <rg-name> \
--enabled true --action LoginWithAzureActiveDirectory \
--aad-client-id <client-id> --aad-tenant-id <tenant-id>EasyAuth injects claims into the request headers:
export async function securedFunction(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
const userId = request.headers.get("x-ms-client-principal-id");
const userName = request.headers.get("x-ms-client-principal-name");
const encodedPrincipal = request.headers.get("x-ms-client-principal");
if (!userId) {
return { status: 401, body: "Unauthorized" };
}
return { jsonBody: { userId, userName } };
}Function Keys
| Key Type | Scope | Use Case |
|---|---|---|
| Function key | Single function | External callers to specific endpoints |
| Host key (default) | All functions | Internal service-to-service calls |
| Master key | All functions + admin APIs | Management operations only |
Manage keys via CLI:
# List function keys
az functionapp function keys list \
--name <app-name> --resource-group <rg-name> --function-name <func-name>
# Create a new function key
az functionapp function keys set \
--name <app-name> --resource-group <rg-name> --function-name <func-name> \
--key-name mykey --key-value <key-value>Managed Identity for Downstream Services
import { DefaultAzureCredential } from "@azure/identity";
import { SecretClient } from "@azure/keyvault-secrets";
const credential = new DefaultAzureCredential();
const vaultClient = new SecretClient("https://myvault.vault.azure.net", credential);
export async function getSecret(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
const secret = await vaultClient.getSecret("my-secret");
return { jsonBody: { value: secret.value } };
}Azure AD Token Validation
import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions";
import jwt from "jsonwebtoken";
import jwksClient from "jwks-rsa";
const client = jwksClient({
jwksUri: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/discovery/v2.0/keys`,
});
async function validateToken(token: string): Promise<jwt.JwtPayload> {
const decoded = jwt.decode(token, { complete: true });
const key = await client.getSigningKey(decoded?.header.kid);
return jwt.verify(token, key.getPublicKey(), {
audience: process.env.AZURE_CLIENT_ID,
issuer: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/v2.0`,
}) as jwt.JwtPayload;
}
export async function protectedApi(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
const authHeader = request.headers.get("Authorization");
if (!authHeader?.startsWith("Bearer ")) {
return { status: 401, jsonBody: { error: "Missing Bearer token" } };
}
try {
const claims = await validateToken(authHeader.slice(7));
return { jsonBody: { user: claims.preferred_username, roles: claims.roles } };
} catch (error) {
return { status: 403, jsonBody: { error: "Invalid token" } };
}
}
app.http("protectedApi", {
methods: ["GET"],
authLevel: "anonymous",
route: "protected",
handler: protectedApi,
});11. Error Handling
Retry Policies
Configure automatic retries in the function registration:
app.storageQueue("processMessage", {
queueName: "my-queue",
connection: "AzureWebJobsStorage",
handler: processMessage,
retry: {
strategy: "exponentialBackoff",
maxRetryCount: 5,
minimumInterval: { seconds: 1 },
maximumInterval: { seconds: 30 },
},
});Fixed delay strategy:
retry: {
strategy: "fixedDelay",
maxRetryCount: 3,
delayInterval: { seconds: 5 },
},Poison Message Handling
Queue-triggered functions automatically move messages to a poison queue after maxDequeueCount failures (default: 5).
Configure in host.json:
{
"extensions": {
"queues": {
"maxDequeueCount": 5,
"visibilityTimeout": "00:00:30",
"batchSize": 16,
"maxPollingInterval": "00:00:02",
"newBatchThreshold": 8
}
}
}The poison queue name is <original-queue-name>-poison. Monitor it separately:
app.storageQueue("handlePoisonMessages", {
queueName: "order-queue-poison",
connection: "AzureWebJobsStorage",
handler: async (message: unknown, context: InvocationContext) => {
context.error("Poison message received:", message);
// Alert, log to dead-letter store, or attempt manual remediation
},
});Custom Error Responses (HTTP)
export async function apiHandler(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
try {
const body = await request.json() as { id: string };
if (!body.id) {
return { status: 400, jsonBody: { error: "Missing required field: id" } };
}
const result = await processItem(body.id);
return { jsonBody: result };
} catch (error) {
context.error("Unhandled error:", error);
if (error instanceof NotFoundError) {
return { status: 404, jsonBody: { error: error.message } };
}
return { status: 500, jsonBody: { error: "Internal server error" } };
}
}12. Common Patterns
Pattern 1: HTTP API with Cosmos DB
A REST API backed by Cosmos DB using input/output bindings.
import { app, HttpRequest, HttpResponseInit, InvocationContext, input, output } from "@azure/functions";
const cosmosInput = input.cosmosDB({
connection: "CosmosDBConnection",
databaseName: "tododb",
containerName: "items",
sqlQuery: "SELECT * FROM c WHERE c.userId = {userId}",
});
const cosmosOutput = output.cosmosDB({
connection: "CosmosDBConnection",
databaseName: "tododb",
containerName: "items",
});
// GET /api/todos/{userId}
export async function getTodos(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
const items = context.extraInputs.get(cosmosInput) as unknown[];
return { jsonBody: items };
}
app.http("getTodos", {
methods: ["GET"],
authLevel: "function",
route: "todos/{userId}",
extraInputs: [cosmosInput],
handler: getTodos,
});
// POST /api/todos
export async function createTodo(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
const todo = await request.json() as { userId: string; title: string };
const newItem = {
id: crypto.randomUUID(),
userId: todo.userId,
title: todo.title,
completed: false,
createdAt: new Date().toISOString(),
};
context.extraOutputs.set(cosmosOutput, newItem);
return { status: 201, jsonBody: newItem };
}
app.http("createTodo", {
methods: ["POST"],
authLevel: "function",
route: "todos",
extraOutputs: [cosmosOutput],
handler: createTodo,
});Pattern 2: Queue-Triggered Processor with Blob Output
A queue processor that reads messages, processes them, and writes results to Blob Storage.
import { app, InvocationContext, output } from "@azure/functions";
interface ReportRequest {
reportId: string;
type: string;
parameters: Record<string, string>;
}
const blobOutput = output.storageBlob({
path: "reports/{reportId}.json",
connection: "AzureWebJobsStorage",
});
export async function generateReport(message: ReportRequest, context: InvocationContext): Promise<void> {
context.log(`Generating ${message.type} report: ${message.reportId}`);
// Simulate report generation
const report = {
id: message.reportId,
type: message.type,
generatedAt: new Date().toISOString(),
data: await fetchReportData(message.type, message.parameters),
};
// Write report to blob storage
context.extraOutputs.set(blobOutput, JSON.stringify(report, null, 2));
context.log(`Report ${message.reportId} saved to blob storage`);
}
app.storageQueue("generateReport", {
queueName: "report-requests",
connection: "AzureWebJobsStorage",
extraOutputs: [blobOutput],
handler: generateReport,
});
async function fetchReportData(type: string, params: Record<string, string>): Promise<unknown> {
// Business logic to gather report data
return { type, params, rows: [] };
}Pattern 3: Durable Functions Approval Workflow
An end-to-end approval workflow with email notification, human approval, and timeout handling.
import * as df from "durable-functions";
import { app, HttpRequest, InvocationContext } from "@azure/functions";
interface ApprovalRequest {
requestId: string;
requester: string;
amount: number;
description: string;
}
// Client — start the workflow
df.app.client.http("startApproval", {
route: "approvals/start",
handler: async (request: HttpRequest, client: df.DurableClient, context: InvocationContext) => {
const body = await request.json() as ApprovalRequest;
const instanceId = await client.startNew("approvalOrchestrator", { input: body });
return client.createCheckStatusResponse(request, instanceId);
},
});
// Orchestrator — coordinate the workflow
df.app.orchestration("approvalOrchestrator", function* (context: df.OrchestrationContext) {
const request = context.df.getInput() as ApprovalRequest;
// Step 1: Send notification
yield context.df.callActivity("sendApprovalEmail", request);
// Step 2: Wait for human response or timeout (48 hours)
const deadline = new Date(context.df.currentUtcDateTime);
deadline.setHours(deadline.getHours() + 48);
const timerTask = context.df.createTimer(deadline);
const approvalEvent = context.df.waitForExternalEvent("Approval");
const winner = yield context.df.Task.any([approvalEvent, timerTask]);
if (winner === approvalEvent) {
timerTask.cancel();
const decision = approvalEvent.result as { approved: boolean; approver: string; comments: string };
if (decision.approved) {
yield context.df.callActivity("executeApproval", { request, decision });
yield context.df.callActivity("notifyRequester", { request, status: "approved", decision });
return { status: "approved", decision };
} else {
yield context.df.callActivity("notifyRequester", { request, status: "rejected", decision });
return { status: "rejected", decision };
}
} else {
yield context.df.callActivity("notifyRequester", { request, status: "expired" });
return { status: "expired" };
}
});
// Activities
df.app.activity("sendApprovalEmail", {
handler: async (request: ApprovalRequest, context: InvocationContext) => {
context.log(`Sending approval email for request ${request.requestId}`);
// Integration with email service (SendGrid, Graph API, etc.)
},
});
df.app.activity("executeApproval", {
handler: async (input: { request: ApprovalRequest; decision: unknown }, context: InvocationContext) => {
context.log(`Executing approved request ${input.request.requestId}`);
// Process the approved request
},
});
df.app.activity("notifyRequester", {
handler: async (input: { request: ApprovalRequest; status: string; decision?: unknown }, context: InvocationContext) => {
context.log(`Notifying ${input.request.requester} — status: ${input.status}`);
},
});Pattern 4: Timer-Triggered Cleanup with Table Storage
A scheduled function that scans Table Storage for expired records and removes them.
import { app, InvocationContext, Timer, input, output } from "@azure/functions";
import { TableClient } from "@azure/data-tables";
import { DefaultAzureCredential } from "@azure/identity";
export async function cleanupExpiredRecords(myTimer: Timer, context: InvocationContext): Promise<void> {
context.log("Starting expired record cleanup");
const tableClient = new TableClient(
`https://${process.env.STORAGE_ACCOUNT_NAME}.table.core.windows.net`,
"SessionTokens",
new DefaultAzureCredential()
);
const now = new Date().toISOString();
let deletedCount = 0;
// Query for expired records
const entities = tableClient.listEntities({
queryOptions: { filter: `expiresAt lt '${now}'` },
});
for await (const entity of entities) {
try {
await tableClient.deleteEntity(entity.partitionKey!, entity.rowKey!);
deletedCount++;
} catch (error) {
context.warn(`Failed to delete entity ${entity.rowKey}:`, error);
}
}
context.log(`Cleanup complete: ${deletedCount} expired records removed`);
}
app.timer("cleanupExpiredRecords", {
schedule: "0 0 3 * * *", // Daily at 3:00 AM UTC
runOnStartup: false,
handler: cleanupExpiredRecords,
});Knowledge references
references/operational-knowledge.md— compact API surface map, prerequisite matrix, deterministic failure remediation, limits/throttling guidance, and safe-default read-first/apply-second pattern.
Progressive Disclosure — Reference Files
| Topic | File |
|---|---|
| HTTP, Timer, Service Bus, Event Hub, Blob, and Queue triggers; input/output bindings; binding expressions; v4 TypeScript examples | `references/triggers-bindings.md` |
| Orchestrator/activity/entity functions; fan-out/fan-in, chaining, human interaction patterns; DurableTaskScheduler; status query APIs | `references/durable-functions.md` |
| Consumption/Flex/Premium/Dedicated plans; host.json configuration; app settings reference; deployment via CLI, GitHub Actions, Bicep; slot deployment | `references/deployment-config.md` |
| Application Insights integration; structured logging; live metrics; distributed tracing; alert rules; Log Analytics KQL queries | `references/monitoring-diagnostics.md` |