Warmup Guide — Serverless: Functions, Triggers/Bindings & Durable

Zero-to-principal primer for Phase 11: what "serverless" actually is under the hood, the Functions trigger/binding model and the host that runs it, the scale controller that turns an event-source backlog into an instance count (and where cold start comes from), the hosting plans and when each is correct, and — the centerpiece — Durable Functions' event-sourced replay model, why it forces determinism, and the patterns built on it. Every concept goes from what it is to why it exists to the mechanism under the hood (diagrams, tables, code) to production significance to the misconceptions that page people at 2 a.m.

Table of Contents


Chapter 1: What "Serverless" Actually Means

From zero. "Serverless" does not mean "no servers." It means you don't manage, provision, or pay for idle servers — the platform runs your code in response to events, scales it for you, and bills you for what you actually use. There is very much a server; you just never see it, name it, patch it, or pay for it while it sits idle.

What it is. Azure Functions is Azure's event-driven serverless compute. The unit is a function: a small piece of code with exactly one trigger (the event that runs it) and zero or more bindings (declarative I/O). You write the function; the host (the Functions runtime) loads it, invokes it on each event, and a separate scale controller decides how many copies (instances) to run.

Why it exists / production significance. Two reasons. Cost: for spiky or intermittent workloads — process an upload, react to a queue message, run a nightly job — paying for an always-on VM is waste; serverless bills per execution and scales to zero. Operability: no OS to patch, no autoscaler to tune by hand, no capacity to pre-provision. The tradeoff is less control (you don't pick the machine), cold start (Chapter 4), and execution limits (a Consumption function has a default 5-minute, max 10-minute timeout). A principal frames serverless as "the platform owns capacity and scaling; I own the function and its idempotency."

Misconception. "Serverless is always cheaper." No — it's cheaper for spiky and low- duty-cycle workloads. A function pegged at 100% all day is often more expensive than a right-sized VM, because per-execution billing assumes you're idle a lot. The cost model is a shape decision, not a default.

Chapter 2: The Functions Model — Trigger, Function, Bindings

From zero. Three nouns, and getting them straight is half of understanding Functions.

  • A trigger is the one event that invokes a function. Every function has exactly one. The trigger type is the function's reason to exist: an HTTP request (httpTrigger), a new queue message (queueTrigger), a schedule (timerTrigger), an Event Grid event (eventGridTrigger), a Service Bus message (serviceBusTrigger), an Event Hub batch (eventHubTrigger).
  • A binding is a declarative connection to a resource. An input binding reads data in before your code runs (e.g. fetch the blob this event refers to); an output binding writes your return value out (e.g. drop a message on another queue) — without you writing any client SDK code. The trigger is itself a special "trigger binding."
  • The host / runtime is the process that hosts your functions, watches the triggers, resolves the bindings, and invokes your code.

Under the hood — the shape of a function. In the modern model a function declares its trigger and bindings in code (decorators / attributes) or in a function.json:

                    ┌─────────────── the function host ───────────────┐
  queue "uploads"   │  trigger: queueTrigger(queue="uploads")         │
   ──────msg───────►│  input  : blobInput(path="images/{msg.name}")   │
                    │  YOUR CODE: thumbnail = resize(blob)            │
                    │  output : queueOutput(queue="thumbnails") ──────┼──► queue "thumbnails"
                    └─────────────────────────────────────────────────┘

The host reads the trigger, hands your code the message (and the bound input blob), runs it, and writes the return value to the output binding. You wrote resize(blob); the host wrote all the plumbing.

Production significance. Bindings are an operability feature: less client code means less to get wrong, less to test, less to leak credentials in. The principal habit: reach for a binding before an SDK call. The lab's dispatch models exactly this — route the event to the matching function, then fan its return value out to its declared output bindings.

Misconception. "A function can have many triggers." No — one trigger per function. If you need a function to react to two sources, you write two functions (or one trigger plus input bindings). The single-trigger rule is what makes "what invoked this?" always have one answer.

Chapter 3: The Scale Controller — Backlog to Instances

This is the first mechanism a principal must own. "Functions autoscales" is the senior answer; how is the principal answer.

From zero. Separate from the host that runs your code, there is a scale controller. Its job: watch the trigger's event source, measure the backlog of unprocessed work, and decide how many instances (copies of your function app) should be running.

Under the hood — target-based scaling. The modern controller (for queue, Service Bus, Event Hub, Cosmos DB, and Storage triggers) uses target-based scaling: it aims to keep a fixed amount of work per instance. The arithmetic the lab implements:

$$ \text{desired} = \left\lceil \frac{\text{backlog}}{\text{target_per_instance}} \right\rceil $$

Then two clamps and a ramp:

  1. Clamp to the plan ceiling: desired = min(desired, max_instances) (and ≥ 0). You can never exceed the plan's instance cap (Consumption defaults to ~200).
  2. Scale to zero: if backlog == 0, desired = 0. No work, no instances, no bill.
  3. Gradual ramp: the controller changes the instance count by at most a bounded amount per decision (per ~second), so a sudden burst doesn't instantly become hundreds of instances that stampede the queue and your downstream database. The lab's max_scale_out_step models this.
backlog over time          desired (target_per_instance=1, max=100, step=5)
   0  ──────────────────►  0     (scaled to zero)
  3   ──────────────────►  3     (ceil(3/1)=3, within step from 0)
 1000 (from current 0) ──► 5     (ideal 1000, but +5/decision: gradual)
 1000 (from current 95)──► 100   (ideal 1000, clamped to max=100)

Production significance. This is the model behind "how many instances will my 10k-message burst reach, and how fast?" The answer is: it ramps +step per decision toward ceil(10000 / target), capped at max_instances. Sizing max_instances is a blast-radius control — it caps how hard your functions can hammer a shared database during a spike. The gradual ramp is why you don't see 0→500 instances in one second.

Event Hub / Service Bus nuance. For partitioned sources, the controller is also bounded by partition/session count — you can't usefully run more instances than there are partitions to read. So "scale out" has two ceilings: max_instances and the source's partition count. This is the serverless echo of Phase 10's "partitions are your parallelism unit."

Misconception. "Functions scales on CPU." For event-driven triggers it scales on backlog (the event source's queue depth / lag), not CPU. CPU-based scaling is the VM/AKS model; the Functions controller reads the source, which is why an idle-CPU-but-deep-queue function still scales out.

Chapter 4: Cold Start and the Scale-to-Zero Tradeoff

From zero. Cold start is the latency added when an event arrives and there is no warm instance ready to handle it: the platform must allocate an instance, start the runtime, load your code and dependencies, and then run your function. Scale-to-zero (Chapter 3) is the direct cause: when the backlog drains, the controller removes every instance, so the next event hits a cold platform.

Under the hood — the cost. A cold start is roughly:

allocate instance + start worker + load runtime + load YOUR code/deps + JIT  →  first invocation
└──────────────── 100s of ms to several seconds (language- and size-dependent) ──────────────┘

The dominant variable is usually your dependency footprint (a fat package set, a heavy framework, a large container image). .NET/Java pay JIT/JVM warmup; Python/Node pay import time; everyone pays for big dependency trees. Warm invocations skip all of it — the instance is already up.

Production significance — this is the question behind a real incident. "Why is the first request after a quiet period slow?" → scale-to-zero + cold start. The fixes are cost decisions:

LeverWhat it doesCost
Premium / Flex always-readykeep N pre-warmed instances; first event is never coldyou pay for the warm instances
Smaller deploymentfewer/lighter dependencies → faster loadengineering time
Keep-alive / warmup triggerping the function to keep one instance warma little compute
Move latency-critical paths off scale-to-zeroput the hot path on Premium, keep batch on Consumptionsplit the workload

The principal move: decide cold start with a number. If the SLA is "p99 background job within 5 minutes," cold start is noise — stay on Consumption and save the money. If it's "p99 user-facing API under 200 ms," scale-to-zero is unacceptable on the hot path — pay for pre-warmed Premium/Flex. This is a Phase 00 latency-budget decision applied to compute.

Misconception. "Cold start is a bug." It's a tradeoff: the price of scale-to-zero (you pay nothing while idle). You don't "fix" it; you decide whether the workload can tolerate it, and pay for pre-warmed instances only where it can't.

Chapter 5: Hosting Plans — Consumption, Flex, Premium, Dedicated

From zero. The hosting plan is the cost/latency/isolation tradeoff dial for Functions. Picking it is a design decision, not a checkbox.

PlanBillingScale-to-zero / cold startPre-warmedVNetUse it for
Consumptionper execution (GB-s + count)yes / yesnolimitedspiky/intermittent background work; cheapest when idle
Flex Consumptionper second + always-readyyes / mitigatedalways-ready instancesyesthe modern default: fast scale, optional warm instances, network isolation
Premium (Elastic)per pre-warmed + per usedno (min instances) / noyesyeslatency-sensitive, long-running, VNet-bound workloads
Dedicated (App Service)the App Service plan you already pay forno scale-to-zeron/a (always on)yeswhen you already run an App Service plan with spare capacity

Under the hood — the decision. Two questions resolve almost every case:

Does the hot path tolerate cold start (100s of ms–seconds occasionally)?
   yes ──► Consumption  (cheapest; scale-to-zero is fine)
   no  ──► need pre-warmed instances ──► Flex (always-ready) or Premium
Do I need VNet integration / private endpoints / long runs?
   yes ──► Flex or Premium (Consumption can't, or is limited)

Production significance. "We're getting cold-start latency on a user-facing endpoint" → move it to Premium/Flex with always-ready instances. "Our bill is high but the functions are idle most of the day" → you're probably on Dedicated/Premium when Consumption would scale to zero. "The function can't reach our private database" → you need VNet integration, so Flex or Premium, not classic Consumption. The plan is the lever for each.

Misconception. "Always use Premium to avoid cold start." Premium has a minimum number of always-on instances you pay for 24/7. For a function that runs twice an hour, that's burning money to dodge a cold start nobody notices. Match the plan to the duty cycle and the latency SLA, not to fear of cold start.

Chapter 6: Durable Functions — Stateful Orchestration

From zero. Plain functions are stateless and short: an event comes in, the function runs, it ends. But real workflows are stateful and long: "validate the order, then reserve inventory across five warehouses in parallel, then charge payment, then wait up to 72 hours for manager approval, then ship." You cannot hold that in a single function invocation (timeouts, cost, crashes). Durable Functions is the extension that makes stateful, long-running, reliable orchestrations possible on top of stateless Functions.

What it is — two function types.

  • An orchestrator function is the coordinator. It describes the workflow: which activities to call, in what order, in parallel or sequence, with what waits. It is deterministic and does no I/O directly (Chapter 7 explains why).
  • An activity function is the worker: the unit that actually does side-effecting work (call an API, write a row, send an email). Activities are where all the I/O, randomness, and clock-reading lives — because they run once and their results are recorded.
orchestrator (deterministic coordinator)
   │  result = yield ctx.call_activity("Validate", order)
   │  reserved = yield [ctx.call_activity("Reserve", w) for w in warehouses]   # fan-out
   │  yield ctx.call_activity("Charge", order)
   ▼
activities (the side-effecting workers — run once, results recorded)
   Validate   Reserve×5   Charge

Under the hood — durability. The orchestration's state is not held in memory; it's an append-only history (an event-sourced log: OrchestratorStarted, TaskScheduled, TaskCompleted, …) persisted in a storage provider (Azure Storage, Netherite, or MSSQL). That history is the workflow's memory — which is what makes it survive a process crash, a restart, or a 72-hour wait: the orchestrator can be reconstructed from its history at any time. How it's reconstructed is the next chapter, and it's the whole game.

Production significance. Durable is how you do "long-running, reliable, stateful" on serverless without standing up a workflow engine. It's the answer to "coordinate these ten steps with retries, parallelism, and a human approval, and survive a crash in the middle."

Misconception. "An orchestrator is a long-lived process waiting in memory." It is not. It is a function that gets replayed from history; between steps it isn't running at all. Internalize this now — everything about Durable follows from it.

Chapter 7: The Replay Model and Why Determinism Is Mandatory

This is the chapter the whole lab exists for. It is the single most misunderstood — and most interview-probed — mechanism in Azure serverless.

From zero — the replay loop. When an event for an orchestration arrives (an activity finished, a timer fired), the Durable extension does not resume a suspended process. It re-runs the orchestrator function from the very first line, feeding it the recorded history. As the replayed code hits each yield ctx.call_activity(...):

  • if that activity is already in the history (it completed on a prior turn), the engine immediately returns the recorded result — it does not re-run the activity;
  • when the code reaches an activity that is not yet in history (the next step), the engine schedules/executes it, records the result, and the turn ends.

So each "turn" advances the orchestration by replaying everything done so far (fast, from memory) and doing exactly one new thing.

TURN 1   replay from top → reach Validate (not in history) → run it, record result, stop
TURN 2   replay from top → Validate REPLAYS recorded result → reach Reserve (new) → run, record, stop
TURN 3   replay from top → Validate, Reserve REPLAY → reach Charge (new) → run, record, stop
...      every turn re-executes the orchestrator from line 1; only the NEXT step is new

The lab's run_orchestrator is exactly this loop: a cursor walks the history; a recorded event replays its result, the next yielded call executes and appends. Replaying with a partial history versus a full history yields the identical ordered sequence of activity calls — that is the determinism guarantee, proven by test_replay_partial_vs_full_same_call_order.

Why determinism is mandatory — under the hood. Because the orchestrator is re-executed from the top every turn, every line runs many times, once per turn. If any line produces a different value on a later replay, the replayed code takes a different path than the one history recorded — and the engine's history no longer matches the code. Concretely:

TURN 1 the orchestrator ran:  if random() > 0.5: callActivity("A")   # random()=0.7 → called A, recorded
TURN 2 replay re-runs that line: random()=0.3 → it now wants B       # but history says A was called!
       → the replayed action DIVERGES from history → corruption

The platform detects this — it sees the new run schedule a different action than the history records — and raises a non-determinism error. The lab models it precisely: when the yielded CallActivity (name/input) doesn't match the recorded HistoryEvent at the cursor, run_orchestrator raises DeterminismError (test_determinism_error_on_*).

The forbidden list, and the fix. Anything that returns a different value on the next replay is banned inside the orchestrator:

Forbidden in the orchestratorWhy it breaks replayUse instead
DateTime.Now / datetime.utcnow()different time each replayctx.current_utc_datetime (recorded once, replayed)
Guid.NewGuid() / uuid4()different id each replayctx.new_guid() (deterministic)
random()different branch each replaycompute randomness in an activity
HTTP / DB / file I/O directlydifferent result + side effects on every replaycall an activity (runs once, recorded)
Task.Delay / sleepnot durable; lost on restartctx.create_timer(fire_at) (durable timer)

The lab's deterministic current_utc() is exactly this: it reads a recorded timestamp from history, so every replay sees the same "now" (test_current_utc_comes_from_history_on_replay). The rule in one sentence: the orchestrator must be a pure function of its history; all nondeterminism goes through ctx or into an activity.

Production significance. "The orchestration is stuck / behaving weirdly / threw a non-determinism exception" is, nine times out of ten, a clock, a GUID, a random, or an I/O call that snuck into the orchestrator — often added in a code change that altered the call sequence of in-flight orchestrations (changing orchestrator code while instances are running is itself a determinism hazard; you version orchestrators or drain them first). Being able to say "that's a determinism violation, here's the line, here's why replay breaks on it" is the principal signal.

Misconception. "Replay re-runs my activities, so they must be idempotent for that reason." Replay does not re-run completed activities — it replays their recorded results (test_replay_does_not_re_execute_recorded_activities). Activities still should be idempotent (for the retry path and at-least-once delivery, Phase 10), but plain replay doesn't re-execute them. The thing replay forces is orchestrator determinism, not activity re-execution.

Chapter 8: The Durable Patterns

From zero. Durable's power is a small set of named patterns. Knowing which to reach for is the design answer.

  • Function chaining — run activities in sequence, each feeding the next:

    a = yield ctx.call_activity("A", x)
    b = yield ctx.call_activity("B", a)
    c = yield ctx.call_activity("C", b)     # A → B → C
    

    Use for: ordered multi-step processing. (The lab's chaining test.)

  • Fan-out / fan-in — schedule N activities in parallel, await all, aggregate:

    tasks = [ctx.call_activity("Work", item) for item in items]
    results = yield tasks            # all run in parallel (when_all)
    total = yield ctx.call_activity("Aggregate", results)
    

    Use for: parallel processing with a join (process 1000 files, then sum). The fan-in is the hard part to do reliably by hand — Durable records each completion in history so it survives crashes. (The lab's fan-out/fan-in test.)

  • Async HTTP API — start a long job and return immediately:

    client.start_new("Orchestrator", input)  →  202 Accepted
         + Location: .../status?id=...        (statusQueryGetUri)
    client polls the status URL until Completed
    

    Use for: any operation longer than an HTTP timeout. No held connections.

  • Monitor — a recurring check until a condition is met, on a durable timer:

    while not done:
        status = yield ctx.call_activity("Check", resource)
        if status == "ready": break
        yield ctx.create_timer(ctx.current_utc_datetime + interval)   # durable wait
    

    Use for: poll-until-ready, with waits that survive restarts (and can run for days).

  • Human interaction — wait for an external event, with a timeout:

    approval = ctx.wait_for_external_event("Approval")
    timeout  = ctx.create_timer(deadline)
    winner = yield ctx.when_any([approval, timeout])    # race the event vs the deadline
    if winner == timeout: escalate()
    

    Use for: approvals, manual gates — "approve within 72h or escalate." The durable timer is what lets it wait days without holding a thread.

Production significance. When an interviewer describes a workflow, the principal move is to name the pattern: "process-then-aggregate" → fan-out/fan-in; "long job a client waits on" → async HTTP; "approve-or-escalate" → human interaction with a timer. Naming it is most of the design.

Misconception. "Fan-out/fan-in needs me to track which parallel tasks finished." No — the history tracks it. Each parallel activity's completion is a recorded event; replay reconstructs "which of the N are done" from history, which is why the join survives a crash.

Chapter 9: Identity, Events, and Where Functions Sit in the Platform

From zero. Functions don't live alone — they're the integration tier that the JD's "integrate serverless applications with Azure security services" is about. Three wires matter:

  • Triggered by events (P10). A function's trigger is usually a Service Bus message or an Event Grid event from Phase 10. The scale controller reads that source's backlog. So Functions is the consumer side of the event-driven architecture you built in P10.
  • Authenticated by Managed Identity (P12). A function calling Key Vault, a database, or another API uses a Managed Identity — an Entra identity (P03) with no stored secret; it gets a token from IMDS (P12). "Where's the function's connection string?" → ideally nowhere; it uses its identity.
  • Fronted by API Management (P09). When a function is an HTTP API, API Management (P09) sits in front: it validates the JWT (P03), enforces rate limits, and routes — so the function focuses on logic, not gatekeeping.
Event Grid / Service Bus (P10) ──trigger──► Function ──Managed Identity (P12)──► Key Vault / DB
                                              ▲
                            API Management (P09, JWT P03) for HTTP APIs

Production significance. A serverless design review checks all three: what triggers it (and how does it scale on that source), how does it authenticate outbound (identity, not secrets), and what fronts it (APIM for HTTP). Missing any one is a finding.

Misconception. "The function needs the database password in app settings." Prefer a Managed Identity with an RBAC grant on the resource — no secret to leak, no rotation, no connection string in config. Secrets in app settings are the old way; identity is the default now.

Lab Walkthrough Guidance

Lab 01 — Functions Runtime: Scale Controller + Durable Replay, suggested order:

  1. desired_instances / scale_decision (Ch. 3–4) — validate inputs, compute ideal = ceil(backlog / target_per_instance) (but 0 when backlog == 0 — scale to zero), clamp to [0, max_instances], then move from current by at most max_scale_out_step in either direction without overshooting. scale_decision compares the result to current. Test scale-to-zero, the max clamp, and the per-step burst limit.
  2. Trigger / Binding / FunctionDef / dispatch (Ch. 2) — Trigger.matches is same-type and (no filter or matching filter); Binding validates direction; dispatch routes to the one matching function, runs it, and fans the return value out to every out binding. Test routing, output bindings, and the no-match error.
  3. run_orchestrator — the replay loop (Ch. 6–7) — walk a cursor over a copy of history. For each yielded CallActivity: if a recorded event exists at the cursor, verify it matches (kind/name/input) — mismatch → DeterminismError — and return its recorded result; else execute activities[name], append a HistoryEvent, return the fresh result. Drive the generator with next/.send. Test chaining's final result and the identical call order on partial vs full replay.
  4. Fan-out / fan-in (Ch. 8) — when the orchestrator yields a list of CallActivity, resolve each element in order (each consumes one history slot) and .send back a list of results. Test the aggregation and identical replay.
  5. Deterministic current_utc() (Ch. 7) — a yielded CurrentUtc replays a recorded timestamp from history, or (on a fresh step) pops the next provided deterministic timestamp and records it. Test that replay reproduces the same value.
  6. DeterminismError (Ch. 7) — confirm a divergent orchestrator (different name, or different input) raises it.

Run it red, make it green: pytest test_lab.py -v, then LAB_MODULE=solution pytest test_lab.py -v, then python solution.py for the worked scale table, chaining, fan-out/ fan-in, and a caught divergence.

Success Criteria

You are ready for Phase 12 when you can, from memory:

  1. Explain trigger vs binding, and why a function has exactly one trigger.
  2. Compute the scale controller's desired instance count for a backlog, and explain the max clamp, the per-step ramp, and the scale-to-zero rule.
  3. Explain precisely where cold start comes from (scale-to-zero) and name the levers (Premium/Flex pre-warmed, smaller deps, keep-alive) and when each is worth its cost.
  4. Pick Consumption vs Flex vs Premium vs Dedicated for a workload and justify it with the latency/cost/isolation/VNet tradeoff.
  5. Explain the Durable replay model — the orchestrator is re-run from history every turn, completed activities replay their recorded results, only the next step executes.
  6. State why an orchestrator must be deterministic and list four things forbidden inside it (clock, GUID, random, I/O) and the ctx API that replaces each.
  7. Name all five Durable patterns and the shape of each, and explain how fan-out/fan-in's join survives a crash (history).

Interview Q&A

Q: How does the Azure Functions scale controller decide how many instances to run? It watches the trigger's event source and reads the backlog — queue depth, Service Bus message count, Event Hub lag — not CPU. With target-based scaling it computes desired = ceil(backlog / target_per_instance), clamps it to the plan's max instances, scales to zero when the backlog is empty, and ramps gradually (a bounded number of instances added per decision) so a burst doesn't stampede the source or downstream. For partitioned sources it's also capped by partition count. So a 10k-message burst climbs toward ceil(10000 / target) a few instances per second, capped at max_instances.

Q: Where does cold start come from, and how do you deal with it? From scale-to-zero: when the backlog drains the controller removes every instance, so the next event has no warm instance and must allocate one, start the runtime, and load my code and dependencies — adding hundreds of ms to seconds, dominated by dependency size. I decide it with the latency budget: if the SLA is a 5-minute background job, cold start is noise — stay on Consumption and save money. If it's a sub-200 ms user-facing path, I move it to Premium or Flex with always-ready / pre-warmed instances (paying for warm capacity), shrink the deployment, or keep the hot path off scale-to-zero. Cold start is a tradeoff to decide, not a bug to fix.

Q: Consumption vs Premium vs Flex — when do you pick each? Consumption for spiky/intermittent background work — cheapest, scales to zero, accepts cold start. Premium (Elastic) when I need no cold start (pre-warmed instances), VNet integration, or long runs — but I pay for a minimum of always-on instances. Flex Consumption is the modern default: per-second billing, fast scale, optional always-ready instances, and VNet integration — Consumption's economics with Premium's capabilities. Dedicated (App Service) when I already run an App Service plan with spare capacity. The plan is the cost/latency/isolation dial.

Q: Why must a Durable orchestrator be deterministic? What breaks if it calls DateTime.Now? Because Durable runs the orchestrator by replaying it from its event-sourced history on every event — re-executing it from the first line each turn, replaying recorded results for completed activities and executing only the next step. So every line runs many times. If a line returns a different value on a later replay — DateTime.Now, Guid.NewGuid(), random(), direct I/O — the replayed code takes a different path than history recorded, the scheduled actions no longer match the log, and the orchestration is corrupted. The platform detects this and throws a non-determinism error. So all nondeterminism goes through the ctx APIs (current_utc_datetime, new_guid, durable timers) or into activities (which run once and record their result). The orchestrator must be a pure function of its history.

Q: Walk me through fan-out/fan-in and how replay reconstructs state. The orchestrator schedules N activities in parallel (yield a list of activity tasks), awaits them all, then aggregates the results in a final activity. Each parallel activity's completion is a recorded history event. On replay, the engine doesn't re-run the completed activities — it replays their recorded results and reconstructs "which of the N are done" straight from history. That's why the join survives a crash mid-fan-out: the durable history, not in-memory state, tracks progress. The lab proves this — run_orchestrator resolves a yielded list element-by-element against history and reproduces the identical call order on replay.

Q: Replay re-runs the orchestrator — does it re-run my activities too? No. Replay re-executes the orchestrator code, but for any activity already in history it returns the recorded result — it does not re-invoke the activity. Only the next un-recorded activity actually runs. (The lab proves it: replaying with completed activities in history never calls them again.) Activities should still be idempotent for the retry path and at-least-once delivery, but plain replay doesn't re-execute them — what replay forces is orchestrator determinism, not activity re-execution.

Q: A function needs to read a secret database connection — how should it authenticate? With a Managed Identity (P12), not a stored secret. The function has an Entra identity that gets a token from IMDS with no credential to leak or rotate; I grant that identity RBAC (or a Key Vault access policy) on the resource. Connection strings in app settings are the old way — identity is the default. The function is triggered by Service Bus/Event Grid (P10), authenticates outbound by identity (P12), and if it's an HTTP API it's fronted by API Management (P09) validating the JWT (P03).

References