Background
Sections
IntroductionFoundations1. Resource Hierarchy2. Resource Manager3. Identity and RBAC4. Regions and Availability5. Naming and TaggingVirtual Machines1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetVirtual Network1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetBlob Storage1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure SQL Database1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Kubernetes Service1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Container Registry1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetMicrosoft Entra ID1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure RBAC1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Functions1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAPI Management1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure App Configuration1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Machine Learning1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Monitor1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure AI Foundry1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and Cheatsheet

3. Architecture

11 min read

The machinery most tutorials skip. Everything that surprises people in production — cold starts, throttling, duplicate executions, a firewalled storage account killing all triggers — falls out of the four mechanisms on this page.

The invocation path, end to end

Trace one HTTP request through the system:

  1. DNS and the front end. myapp.azurewebsites.net resolves to the App Service front-end fleet — a shared, multi-tenant layer of load balancers that terminates TLS and routes by hostname. Your app does not own this; it is the same front end App Service uses.
  2. Site routing. The front end finds the worker(s) currently assigned to your site. If there are none — the app is scaled to zero — this is where the request waits while one is allocated.
  3. Worker allocation (cold path only). The platform picks a worker from a pool of pre-warmed VMs, mounts your app content, and starts the Functions host.
  4. Host start. The host reads host.json, loads the binding extensions, and enumerates your functions and their triggers.
  5. Language worker start. For non-.NET-in-process apps, a separate worker process starts — Node, Python, Java, or your isolated .NET process — and registers over gRPC. Your dependency tree loads here. This is usually the largest, most controllable slice of cold start.
  6. Binding resolution. Input bindings are evaluated: the blob is fetched, the Cosmos document read, the binding expressions substituted.
  7. Your code runs. Finally.
  8. Output bindings flush. Queue messages sent, blobs written, after your function returns successfully. A thrown exception means output bindings do not run — which is a feature, and also the reason a partially-completed function can look like it did nothing.
  9. Telemetry. The invocation, its dependencies, and its traces are sent to Application Insights, sampled according to host.json.

For a queue or event trigger, steps 1–2 are replaced by the scale controller noticing a backlog and the host's listener leasing a message — but steps 3–9 are identical.

Everything that happens between a request arriving and your first line of code running

The scale controller

The single most Azure-specific piece of this service, and the one with no Lambda equivalent.

Your function app doesn't decide how many instances it needs. A separate Azure-managed component — the scale controller — watches your trigger sources on your behalf and adjusts the instance count. For a queue trigger it inspects queue length and the age of the oldest message; for Event Hubs it inspects the unprocessed event count per partition; for HTTP it watches request rate and latency.

Two implications people miss:

The scale controller reads your trigger source directly. It connects to the storage account, Service Bus namespace, or Event Hub itself, outside your function's execution. If you lock that source down to a private endpoint, the scale controller can lose visibility and your app stops scaling out even though the code works fine. The mitigation is runtime-driven scaling (where the host, inside your network, reports scale demand instead) — available on Premium and Flex ⚠️ verify current support matrix against current Azure docs. This is one of the top three production surprises in this service.

Scaling is per-app, not per-function. All functions in an app share one instance count. A busy queue-triggered function will scale out the app, and your rarely-used HTTP function comes along for the ride — inheriting its warm instances, which is sometimes a happy accident and sometimes a cost surprise. Conversely, one function's slow dependency can starve the others. Split apps by scaling characteristic.

Target-based scaling is the newer algorithm: rather than adding one instance per scaling interval, the controller computes a target count from backlog ÷ per-instance concurrency and jumps there. It reacts to spikes far faster than the older incremental approach, and it makes the host.json concurrency settings genuinely load-bearing rather than advisory.

Bounds on all of this:

  • Event Hubs and Cosmos DB change feed scale by partition. More instances than partitions is wasted; the partition count is your real parallelism ceiling.
  • Service Bus sessions scale by session, similarly.
  • HTTP scales by request pressure with no such natural bound, which is why an HTTP function in front of a small database is the classic way to melt the database.

Backlog on the trigger sources — not anything in your code — decides how many instances you get

Cold start, dissected

"Cold start" is four separate delays that people quote as one number. Knowing which one you have determines which fix works.

Stage What's happening What reduces it
Worker allocation The platform assigns a VM and mounts your content Always-ready (Flex) or pre-warmed (Premium) instances. Nothing in your code helps
Host start The Functions host boots and loads binding extensions Fewer extensions; keeping the extension bundle current
Language worker + dependency load Your runtime starts and imports your dependencies Smaller packages, fewer imports, lazy initialisation, run-from-package deployment
First-call initialisation Your own connection pools, HTTP clients, SDK clients Create clients once at module/static scope, never per invocation

Two practical rules that matter more than any platform setting:

  • Never construct SDK clients inside the handler. A new HttpClient() or a fresh Cosmos client per invocation is the single most common performance defect in Functions, and it also causes socket exhaustion under load. Hoist them to static/module scope so they're reused across invocations on the same instance.
  • Deploy as a package, not as loose files. Running from a mounted package (WEBSITE_RUN_FROM_PACKAGE on classic plans, and the default deployment model on Flex Consumption) makes app content read-only and dramatically speeds start compared to synchronising thousands of files.

The "keep it warm with a timer ping" trick works on Consumption but is a workaround, not a design. If cold start genuinely violates your SLO, buy always-ready capacity and stop fighting the platform.

Control plane vs. data plane

Azure's split is sharper than AWS's, and for Functions it's sharper than for most Azure services because there are effectively three planes.

Plane Endpoint What it does Who governs it
Control plane management.azure.com (ARM) Create/update/delete the app and plan, read and write app settings, restart, swap slots, read the publishing credentials and keys Azure RBAC — Contributor, Website Contributor
Data plane myapp.azurewebsites.net Invoke your functions Function/host keys by default, or Entra ID if you enable App Service Authentication. Azure RBAC does not apply here by default
SCM / Kudu plane myapp.scm.azurewebsites.net Deploy, browse the file system, run console commands, read logs Publishing credentials, or Entra ID with the right control-plane permission

The classic mistake: granting someone Reader on the function app and expecting them to be unable to call it — or granting Contributor and thinking that's a data-plane grant. Neither is true. Reader cannot list the keys, so in practice it does limit invocation of a key-protected function; Contributor can list the keys, and therefore can invoke anything. Control-plane Contributor is, for a key-protected function app, effectively full data-plane access. Say that plainly in a design review.

The second classic mistake: forgetting that the SCM plane is a separate front door with separate access rules. Locking the app's public network access without locking SCM leaves a deployment endpoint exposed; locking SCM without thinking leaves your CI/CD unable to deploy.

Where the keys actually live. Function and host keys are stored — by default — in blob storage in the AzureWebJobsStorage account. They can instead be kept in Key Vault ⚠️ verify current support and configuration against current Azure docs. This is another reason that storage account is more security-relevant than it looks.

Three front doors into the same app, each opened by a different kind of credential

The storage account dependency

Worth its own section because it causes outages that look like code bugs.

The Functions host uses the account in AzureWebJobsStorage for:

  • Trigger metadata and listener state — including blob receipts for the polling blob trigger.
  • Timer schedules and singleton leases — so exactly one instance runs a scheduled occurrence.
  • Event Hubs and Cosmos DB checkpoints/leases — where in the stream each partition has reached.
  • Durable Functions task hubs — the entire orchestration history.
  • Function and host keys, by default.
  • The deployment package, on most deployment models.

Consequences:

  • Firewalling the storage account without exceptions breaks the app. Not partially — timers stop firing, Durable orchestrations stall, and key lookups fail. If you restrict it, you must configure the app's network integration, private endpoints for the relevant sub-resources (blob, file, queue, table), and — on classic plans using content share — the file share settings. Test this in a lower environment; it is fiddly and it fails at the worst time.
  • Two apps must not share a task hub name. Two function apps pointed at one storage account with default Durable configuration will interleave their orchestration state.
  • Storage transactions are a real cost line. A polling blob trigger over a large container, or a chatty Durable orchestration, can generate more spend in storage transactions than in compute.

Consistency, delivery, and durability

  • Delivery is at-least-once for the queue-shaped triggers (Storage queue, Service Bus, Event Hubs). Retries, host restarts, and scale-in can all cause a message to be processed twice. Idempotency is your responsibility — carry an idempotency key and make the write conditional.
  • Ordering is not guaranteed except within an Event Hubs partition or a Service Bus session, and even then only if concurrency is constrained appropriately.
  • Retries come from two places, and confusing them causes duplicated effort: the trigger's own semantics (a Service Bus message is redelivered until max delivery count, then dead-lettered) and the host's retry policy configured in host.json. Prefer the trigger's native semantics where they exist; they're visible, countable, and have a dead-letter destination.
  • Output bindings are not transactional. A function that writes to two output bindings can fail between them. If you need atomicity, use one write and derive the rest, or adopt an outbox pattern.
  • Durable Functions replay orchestrator code. This is not a retry — it is how the framework rebuilds in-memory state from an event history. Non-deterministic code in an orchestrator (timestamps, GUIDs, direct I/O) produces bugs that only appear after a restart.

Failure modes

The list to recognise on sight:

  • HTTP 429 from your dependencies, not from Functions. Functions scaling out is precisely what causes throttling downstream — Cosmos DB 429s, SQL connection-pool exhaustion, a rate-limited third-party API. The fix is a concurrency ceiling in host.json or a queue in front, not a bigger plan. Serverless compute in front of a non-serverless datastore is the single most common architectural failure in this service.
  • Cold-start latency spikes on a low-traffic HTTP app, seen as a p99 that is 50× the p50.
  • SNAT port exhaustion. Outbound connections from an app share a limited pool of source ports. Creating a new client per invocation, or hammering an external endpoint, exhausts them and produces intermittent connection timeouts that look like the remote service is down ⚠️ verify current limits. Reused clients and private endpoints both help.
  • Silent trigger failure after a network change. Timers stop, queues back up, no errors in your code. Look at storage account networking and scale-controller access first.
  • Poison messages looping. A message that always fails is retried to the max, then dead-lettered — or, for Storage queues, moved to a -poison queue nobody monitors. Alert on dead-letter depth.
  • Host restarts mid-execution. Platform upgrades, scale-in, and configuration changes all restart the host. A long-running non-Durable function will be killed mid-flight. Design for resumption.
  • Regional outage. A function app is a regional resource. Multi-region means two apps and a traffic manager in front — see Production.

Scaling ceilings and where they're counted

The Azure-specific part of "what's the limit" is always at what scope:

Limit Typical scope
Maximum scale-out instances Per function app, varying by plan and OS
Instances available to you overall Per subscription, per region — and shared with other App Service workloads
App Service Plan instance count Per plan
Storage account transaction rate Per storage account — shared by every app pointed at it
Event-driven parallelism Per partition (Event Hubs, Cosmos DB) or per session (Service Bus)

⚠️ Every number behind these rows varies by plan, operating system, region, and subscription type — verify against current Azure docs before designing to one. The durable lesson is the shape: your ceiling is usually your downstream dependency or your partition count, not the Functions plan.


Next: Getting Started →

← Back to the Azure Functions overview · ← Previous: Core Concepts