🛸 Hitchhiker's Guide — Phase 11: Serverless, Functions, Triggers/Bindings & Durable

Read this if: you've written an Azure Function that worked, but couldn't explain how it scaled, where cold start comes from, or why a Durable orchestrator can't call DateTime.Now. This is the compressed tour the WARMUP derives slowly. Skim it, read the WARMUP, then build the runtime.


0. The 30-second mental model

A trigger invokes your function; bindings wire its I/O; the host runs it; and a separate scale controller decides how many copies to run by reading the trigger source's backlogdesired = ceil(backlog / target_per_instance), capped at the plan max, ramped gradually, and scaled to zero when idle (which is where cold start comes from).

For stateful workflows, Durable Functions runs your orchestrator by replaying it from its recorded history on every event — completed activities replay their saved results, only the next step runs. One sentence to tattoo: a Durable orchestrator is replayed, not run — so it MUST be deterministic (no clock, no random, no GUID, no I/O — those go in activities or through ctx).

1. Trigger / function / bindings in one breath

ONE trigger per function = the event that invokes it
   httpTrigger | queueTrigger | timerTrigger | eventGridTrigger | serviceBusTrigger | eventHubTrigger
bindings = declarative I/O (no client SDK code)
   input binding  → data handed to your code (read this blob)
   output binding → your return value written out (drop on this queue / write this Cosmos doc)
host = loads the function, watches triggers, resolves bindings, invokes your code
scale controller = SEPARATE; reads the trigger source backlog → instance count

2. The scale controller (the part "autoscales" hides)

desired = ceil(backlog / target_per_instance)      # per-INSTANCE work target, not CPU
desired = min(desired, max_instances)               # plan ceiling (Consumption ~200)
desired = 0  if backlog == 0                         # scale to zero
move from current by ≤ step per decision             # gradual ramp, no stampede
also capped by partition/session count for Event Hub / Service Bus sessions

It scales on backlog, never CPU, for event triggers. The gradual ramp is why a 10k burst doesn't become 10k instances in one second — it climbs +step per decision toward the cap.

3. The numbers to tattoo on your arm

ThingValue
Consumption instance cap~200 (Windows; Linux ~100)
Consumption timeoutdefault 5 min, max 10 min
Premium/Dedicated timeoutup to unbounded (Premium default 30 min)
Scaleon backlog (queue depth / lag), not CPU
Scale to zeroConsumption + Flex (→ cold start)
Cold start~100s of ms to several seconds, dominated by dependency size
Pre-warmed / no cold startPremium (always min instances), Flex (always-ready)
Event-trigger parallelism ceilingalso partition/session count of the source
Durable orchestrator ruledeterministic — no DateTime.Now/random/Guid/I/O
Durable stateevent-sourced history in Azure Storage / Netherite / MSSQL
Replay-safe timecontext.current_utc_datetime (NOT DateTime.Now)
Replay-safe idcontext.new_guid() (NOT Guid.NewGuid())
Replay-safe waitcontext.create_timer() (NOT Task.Delay/sleep)

4. Hosting plan in one glance

spiky / intermittent, cold start OK     →  Consumption    (cheapest, scale-to-zero)
modern default: fast scale + warm + VNet →  Flex Consumption (always-ready optional)
no cold start / long runs / VNet         →  Premium (Elastic)  (pay for min warm instances)
already have spare App Service capacity   →  Dedicated

Decision: does the hot path tolerate cold start? No → pre-warmed (Flex/Premium). Need VNet/private endpoints? Yes → Flex/Premium.

5. The five Durable patterns (name the right one = the design)

chaining        A → B → C, each feeds the next                (sequential steps)
fan-out/fan-in  N activities in parallel, await all, aggregate (parallel + join; history tracks completion)
async HTTP      start → 202 + statusQueryGetUri, client polls  (job longer than an HTTP timeout)
monitor         poll on a durable timer until condition met    (poll-until-ready, days-long)
human interaction  wait_for_external_event raced vs a timer    (approve within 72h or escalate)

6. The replay model in one picture

TURN 1  replay from top → first uncompleted activity runs, records result, STOP
TURN 2  replay from top → completed activity REPLAYS its recorded result → next runs, STOP
...     orchestrator re-executes from line 1 EVERY turn; only the NEXT step is new
        completed activities are NOT re-run — their results replay from history
non-determinism (clock/random/GUID/I/O) → later replay yields a DIFFERENT action
        than history recorded → the platform throws a non-determinism error

7. func / az one-liners you'll actually type

# Scaffold a Functions project + an HTTP function (Core Tools)
func init MyFuncs --worker-runtime python
cd MyFuncs && func new --name HttpStarter --template "HTTP trigger"
func start                                   # run locally

# Create a Consumption function app (needs a storage account + a plan)
az functionapp create -g rg -n my-func --consumption-plan-location eastus \
  --runtime python --functions-version 4 --storage-account mystorage --os-type Linux

# Create a Flex Consumption app (modern default: fast scale, VNet, always-ready)
az functionapp create -g rg -n my-flex --flexconsumption-location eastus \
  --runtime python --storage-account mystorage

# Premium plan (pre-warmed, no cold start) then an app on it
az functionapp plan create -g rg -n my-premium --location eastus --sku EP1 --min-instances 1
az functionapp create -g rg -n my-app --plan my-premium --runtime dotnet-isolated --storage-account mystorage

# Turn ON a Managed Identity for the function (so it authenticates with NO secret — P12)
az functionapp identity assign -g rg -n my-func

# Cap how hard it can scale out (blast-radius control on a shared DB)
az functionapp config set -g rg -n my-func --generic-configurations '{"functionAppScaleLimit": 50}'

# Deploy + tail logs
func azure functionapp publish my-func
az functionapp log tail -g rg -n my-func

# Inspect a Durable orchestration's status (the management API)
curl "https://my-func.azurewebsites.net/runtime/webhooks/durabletask/instances/<id>?code=<key>"

8. War story shapes you'll relive

  • "First request after a quiet period is slow." → scale-to-zero + cold start. The next event hit a cold platform. Fix: Premium/Flex pre-warmed on the hot path only, or shrink deps — and only if the SLA actually needs it.
  • "My burst stampeded the database." → the function scaled out hard and N instances all hammered one DB. Cap functionAppScaleLimit (blast radius) and/or use a connection pool / Service Bus to smooth it. The gradual ramp helps but the cap is the real control.
  • "The orchestration threw a non-determinism exception." → a DateTime.Now, Guid, random(), or direct I/O snuck into the orchestrator. Move it to an activity or use the ctx API. Classic: someone added a log line with DateTime.Now.
  • "I changed the orchestrator code and in-flight orchestrations broke." → changing the orchestrator's call sequence while instances are mid-replay diverges from their history. Version the orchestrator (new name) or drain before deploying.
  • "It works on Event Hub but won't scale past 4 instances." → the source has 4 partitions; you can't usefully run more consumers than partitions. The ceiling is partition count, not just max_instances.
  • "My function 'lost' the long wait after a restart." → you used Task.Delay/sleep instead of a durable timer (create_timer). Plain sleeps don't survive a restart; durable timers are recorded in history.

9. Vocabulary that signals you've held the pager

  • Scale controller — the component that turns event-source backlog into instance count.
  • Target-based scalingceil(backlog / target_per_instance); the per-instance work target.
  • Scale to zero — no work → no instances → the cause of cold start.
  • Cold start — startup latency when no warm instance exists; dominated by dependency size.
  • Pre-warmed / always-ready — Premium/Flex instances kept warm to kill cold start.
  • Trigger vs binding — the one event that invokes vs the declarative I/O wires.
  • Orchestrator vs activity — the deterministic coordinator vs the side-effecting worker.
  • Replay / event sourcing — the orchestrator is re-run from its recorded history each event.
  • Determinism / non-determinism error — the orchestrator must be a pure function of its history; the platform throws when a replay diverges.
  • Fan-out/fan-in — parallel activities + a join the history tracks across crashes.
  • Durable timer — a replay-safe, restart-surviving wait (create_timer).
  • continue_as_new — reset an eternal orchestration's history so it doesn't grow forever.

10. Beginner mistakes that mark you in interviews

  1. Saying "Functions autoscales" with no idea it scales on backlog, not CPU.
  2. Not knowing where cold start comes from (it's scale-to-zero), or "fixing" it everywhere with Premium when most paths don't need it (paying for idle warm instances).
  3. Calling a Durable orchestrator "a long-running process in memory" (it's replayed).
  4. Putting DateTime.Now / Guid.NewGuid() / random() / an HTTP call in the orchestrator — the determinism violation that corrupts replay.
  5. Thinking replay re-runs completed activities (it replays their recorded results).
  6. Using Task.Delay/sleep for a long wait instead of a durable timer.
  7. Forgetting the Event Hub / Service Bus parallelism ceiling is partition/session count.
  8. Storing the DB connection string in app settings instead of using a Managed Identity.
  9. Changing orchestrator code while orchestrations are in-flight (breaks their replay).

11. How this phase pays off later

  • Managed Identity (P12) is how every function authenticates outbound with no secret — this phase is the compute, P12 removes the credential.
  • Event Grid / Service Bus (P10) are the triggers whose backlog the scale controller reads — Functions is the consumer side of P10's event plumbing.
  • API Management (P09) fronts HTTP functions, validating the JWT (P03) you already built.
  • Latency budgets (P00) are how you decide whether cold start is acceptable — compute the budget, then pick Consumption vs pre-warmed.
  • Reliability (P14) — Durable's retries, timers, and idempotent activities are the reliability primitives applied to workflows.

Now read the WARMUP slowly, then build the runtime. After this, "serverless" stops being a buzzword and becomes two mechanisms you can draw: backlog→instances, and replay→determinism.