All topics
Cloud · Learning hub

Azure Functions notes for developers

Master Azure Functions with a curated set of 1 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallTest yourself — Azure Functions quizMore Cloud notes
Azure Functions

Azure Functions Essentials

Azure Functions Essentials What It Is: Triggers & Bindings Azure Functions is Microsoft's serverless compute platform — you write a single function, declare wha

Azure Functions Essentials

What It Is: Triggers & Bindings

Azure Functions is Microsoft's serverless compute platform — you write a single function, declare what invokes it (a trigger) and what data flows in/out of it (bindings), and Azure handles hosting, scaling, and the event plumbing. The defining Azure-specific concept is the triggers-and-bindings model: instead of manually calling SDKs inside your function body to read a queue message or write a blob, you declare input/output bindings in configuration and the runtime injects/collects the data for you. Exactly one trigger per function; any number of input/output bindings.

  • Trigger — what invokes the function: HTTP, Timer, Queue Storage, Service Bus, Event Grid, Blob Storage, Cosmos DB change feed

  • Input binding — data pulled in automatically before your code runs (e.g. a Cosmos DB document by id from the route)

  • Output binding — data your function returns that Azure writes out for you (e.g. push a message to a Service Bus queue without an SDK call)

  • Function App — the deployment/hosting unit that groups one or more functions and shares config, scaling, and a hosting plan

HTTP & Queue Triggers (Node.js v4 model)

const { app } = require('@azure/functions')

// HTTP trigger — the most common entry point for APIs
app.http('createOrder', {
  methods: ['POST'],
  authLevel: 'function',        // anonymous | function | admin
  route: 'orders',
  handler: async (request, context) => {
    const body = await request.json()
    if (!body.items || body.items.length === 0) {
      return { status: 400, jsonBody: { error: 'items is required' } }
    }

    context.log(`Creating order for ${body.customerId}`)
    const order = await createOrder(body)

    // Output binding: push a message onto a queue for async processing
    context.extraOutputs.set(queueOutput, JSON.stringify({ orderId: order.id }))

    return { status: 201, jsonBody: order }
  },
})

// Queue trigger — fires when a message lands in the queue
app.storageQueue('processOrder', {
  queueName: 'order-processing',
  connection: 'AzureWebJobsStorage',
  handler: async (queueItem, context) => {
    const { orderId } = JSON.parse(queueItem)
    context.log(`Processing order ${orderId}`)
    await fulfillOrder(orderId)
  },
})

// Timer trigger — CRON-style schedule
app.timer('nightlyCleanup', {
  schedule: '0 0 2 * * *',       // 2 AM daily, NCronTab format (includes seconds)
  handler: async (myTimer, context) => {
    context.log('Running nightly cleanup')
    await purgeExpiredSessions()
  },
})

Durable Functions: Orchestration

Durable Functions is an extension for writing stateful, long-running workflows in normal-looking code — orchestrations that survive restarts, coordinate multiple functions, and can run for hours or months. The orchestrator function itself must be deterministic (no direct I/O, no random/DateTime.Now calls) because the runtime replays its history to rebuild state after every checkpoint; all real work happens in activity functions the orchestrator calls out to.

const df = require('durable-functions')

// Orchestrator — deterministic, describes the workflow shape
df.app.orchestration('processOrderOrchestrator', function* (context) {
  const order = context.df.getInput()

  const paymentResult = yield context.df.callActivity('chargePayment', order)
  if (!paymentResult.success) {
    yield context.df.callActivity('notifyFailure', order)
    return { status: 'failed' }
  }

  // Fan-out/fan-in: run these activities in parallel, wait for all
  const tasks = [
    context.df.callActivity('reserveInventory', order),
    context.df.callActivity('notifyWarehouse', order),
  ]
  yield context.df.Task.all(tasks)

  // Durable timer — sleeps without holding compute or billing for wait time
  yield context.df.createTimer(
    new Date(context.df.currentUtcDateTime.getTime() + 24 * 60 * 60 * 1000)
  )

  yield context.df.callActivity('sendReviewRequest', order)
  return { status: 'completed' }
})

// Activity — where actual I/O happens
df.app.activity('chargePayment', {
  handler: async (order) => {
    return await paymentGateway.charge(order.total, order.paymentToken)
  },
})

// HTTP trigger that starts the orchestration
df.app.orchestration('processOrderOrchestrator')
app.http('startOrderProcessing', {
  methods: ['POST'],
  extraInputs: [df.input.durableClient()],
  handler: async (request, context) => {
    const client = df.getClient(context)
    const order = await request.json()
    const instanceId = await client.startNew('processOrderOrchestrator', { input: order })
    return client.createCheckStatusResponse(request, instanceId)
  },
})

Hosting Plans & Scaling

The hosting plan you pick determines cold starts, scaling behavior, and how you're billed. This is a major architectural decision, not a deploy-time detail.

  • Consumption plan — true pay-per-execution, scales to zero, generous free grant monthly, but cold starts on scale-from-zero and a max 10-minute execution timeout by default

  • Premium plan — pre-warmed instances (no cold starts), VNet integration, longer/unbounded execution, billed for pre-warmed capacity even when idle

  • Dedicated (App Service) plan — runs on VMs you already pay for (e.g. alongside a web app), predictable cost, manual/autoscale rules, no scale-to-zero

# Create a Consumption-plan function app
az functionapp create \
  --resource-group my-app-rg \
  --consumption-plan-location eastus \
  --runtime node \
  --runtime-version 20 \
  --functions-version 4 \
  --name my-func-app \
  --storage-account myfuncstorage001

# Deploy local project
func azure functionapp publish my-func-app

# Stream logs while testing
func azure functionapp logstream my-func-app

# Local dev — Azure Functions Core Tools
func start

Pitfalls & Practical Tips

  • Orchestrator functions must be deterministic — no direct HTTP calls, no Date.now(), no random numbers; the runtime replays orchestrator code from history, so any non-deterministic call must go through a durable API (createTimer, activity calls) instead

  • Consumption plan cold starts can add several seconds of latency on the first request after idle — use Premium plan or a min instance count for latency-sensitive HTTP APIs

  • Queue/blob/Cosmos triggers deliver at-least-once — design activity functions and handlers to be idempotent, since the same message can be retried after a partial failure

  • Consumption plan has a 10-minute default execution timeout (configurable up to 10 min max on Consumption; unbounded on Premium/Dedicated) — long-running work belongs in Durable Functions or a queue-based fan-out, not a single long HTTP handler

  • authLevel: 'function' requires a function key on every request — use 'anonymous' behind API Management or your own auth layer instead of hand-rolling key distribution to clients

  • VNet integration (private resource access) requires the Premium or Dedicated plan — Consumption-plan functions can't join a VNet

Keep your Azure Functions knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever