7. Production
The difference between "it worked in the portal" and "I run this". Five pillars, in the order they usually bite.

Security
Inbound: stop using function keys as your security model.
The default authLevel: function gives you a shared secret in a query string. It has no identity, no
expiry, no per-caller revocation, and it ends up in browser history, proxy logs, and Slack. It is
fine for a webhook from a system that can't do anything better; it is not an authentication strategy.
The ladder, in order of preference:
- App Service Authentication (Easy Auth) with Microsoft Entra ID — the platform validates a bearer token before your code runs. Per-caller identity, revocable, auditable, no secret in the URL. This is the right answer for most internal APIs.
- API Management in front, presenting OAuth outward and holding the function key inward. Right when several teams consume the API and you need throttling and versioning too.
- Function keys, restricted to inbound-webhook cases, rotated on a schedule, and stored in Key Vault rather than pasted into the caller's config.
authLevel: anonymous— legitimate only when something in front (Easy Auth, APIM, Front Door with WAF) is doing the authentication. It is a footgun everywhere else.
Outbound: managed identity, always. The function app's system-assigned identity plus a least-privilege data-plane role assignment replaces every connection string you would otherwise hold. The distinction that trips people up in review:
| You want | Control-plane role | Data-plane role |
|---|---|---|
| Manage the storage account | Contributor |
— |
| Read blobs | ❌ not Contributor |
Storage Blob Data Reader |
| Read a Key Vault secret | ❌ not Key Vault Contributor |
Key Vault Secrets User |
| Receive Service Bus messages | ❌ not Contributor |
Azure Service Bus Data Receiver |
Where the built-in role is too broad. Website Contributor on a function app lets the holder
list keys — and listing keys is, for a key-protected app, equivalent to full data-plane access. If
you need someone to restart apps and read configuration without being able to invoke everything,
write a custom role that excludes Microsoft.Web/sites/host/listkeys/action and
Microsoft.Web/sites/functions/listkeys/action.
Network isolation.
publicNetworkAccess = Disabledplus a private endpoint (sub-resourcesites) for inbound.- VNet integration for outbound — Flex Consumption, Premium, or Dedicated only. Consumption cannot.
- Access restrictions (IP or service-tag rules) as the lighter-weight alternative.
- Do not forget the SCM endpoint. It is a separate sub-resource with separate rules. Leaving it public while locking the main endpoint is a common and serious oversight; locking it without planning breaks your pipeline.
Other hardening worth doing on day one: httpsOnly = true, minimum TLS 1.2, FTPS disabled,
shared_access_key_enabled = false on the backing storage account (with the three data roles granted
— see Deployment), and basic publishing credentials disabled so nobody can
deploy with a username and password.
Encryption. At rest, everything is encrypted with platform-managed keys by default. Customer-managed keys are available for the backing storage account, which is where your Durable state and keys live — consider it when a compliance requirement names CMK specifically, not by default.
Cost
What you actually pay for, by plan:
| Plan | Compute meter | Idle cost |
|---|---|---|
| Consumption | Executions + GB-seconds (memory × duration), after a monthly free grant ⚠️ verify | ~Zero |
| Flex Consumption | Executions + GB-seconds, plus any always-ready instance baseline | The baseline you chose |
Premium (EP1–EP3) |
Per instance, per hour | Never zero — minimum one instance, always |
| Dedicated | The App Service Plan, hourly | The whole plan, always |
And the meters people forget, which frequently exceed the compute line:
- Application Insights ingestion. Billed per GB. A chatty app with verbose logging and no
sampling can spend more on telemetry than on execution. Set sampling in
host.json, set a daily cap on the workspace, and dropInformation-level dependency noise you never read. - Storage transactions. Polling blob triggers, Durable orchestrations, and Event Hub checkpointing all generate high transaction counts. Durable in particular is transaction-dominated.
- Data egress, and any private endpoints (billed hourly, plus processing).
The biggest cost trap: an idle Premium plan. It cannot scale to zero, and an EP1 created for a
one-week VNet experiment bills every hour for years. Search any mature subscription for Elastic
Premium plans with no traffic; you will find one. Azure Policy denying EP* SKUs outside prod is a
five-minute control that pays for itself.
Three concrete optimisations:
- Right-size the plan, and reconsider it quarterly. The Consumption→Premium jump is often made for one reason (usually VNet access or cold start). Flex Consumption may now solve that reason while keeping scale-to-zero economics — re-evaluate rather than inheriting the decision.
- Reduce memory footprint and duration. On GB-seconds pricing these multiply. Lazy-load dependencies, reuse clients, and don't hold large buffers.
- Sample telemetry and cap the workspace. Ten minutes of
host.jsonwork, frequently the largest single saving.
For steady, predictable, high-volume load, run the arithmetic honestly: at constant throughput, Dedicated or a reserved App Service Plan is often cheaper than per-execution pricing. Serverless is elasticity insurance, and insurance has a premium.
Scaling and limits
The Azure-specific discipline: every limit needs a scope, because a number without one is useless.
| Limit | Scope it's counted at |
|---|---|
| Maximum scale-out instances | Per function app (varies by plan and OS) |
| Total instances available | Per subscription, per region — shared with other App Service workloads |
| Plan instance count | Per App Service Plan |
| Execution timeout | Per function invocation, ceiling set by plan |
| Storage 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) |
⚠️ The specific numbers behind each row vary by plan, OS, region, and subscription type — verify against current Azure docs before designing to one.
Hard vs. soft. Instance counts per subscription-per-region are generally soft and raisable via a
quota request (az quota, or a support ticket for App Service capacity). The per-plan and per-invocation
limits are structural — you change them by changing the plan, not by asking.
The ceiling you'll actually hit is downstream. In practice, function apps rarely exhaust their own instance limit. They exhaust the database's connection pool, the Cosmos container's provisioned RU/s, the third-party API's rate limit, or their own SNAT ports. When you see 429s and timeouts under load, look downstream before you look at the plan.
The controls that matter, in the order to reach for them:
- Per-instance concurrency in
host.json—maxConcurrentRequestsfor HTTP,maxConcurrentCallsfor Service Bus,batchSizefor queues. This is your throttle. - A maximum scale-out limit on the app — a hard ceiling on how far the app can grow, which is how you protect a fragile dependency from a traffic spike.
- A queue between ingress and work, converting a burst into a drain.
- Splitting the app, so a hot function's scaling doesn't drag an unrelated one along.
Observability
Diagnostic settings are not on by default. Neither is anything else. A function app with no Application Insights and no diagnostic setting is a black box, and you will find that out during an incident.
Turn on, at minimum:
- Application Insights — connected via
APPLICATIONINSIGHTS_CONNECTION_STRING, ideally a workspace-based component so the data lands in your central Log Analytics workspace. - A diagnostic setting on the app routing
FunctionAppLogsandAllMetricsto Log Analytics. - Sampling configured deliberately in
host.json, so you keep failures and sample successes rather than losing both to a cap.
Metrics worth alerting on:
| Metric / signal | Why | Typical alert shape |
|---|---|---|
| Function error rate | The obvious one | Failures > N over 5 minutes |
| Invocation duration p95 | Catches the slow-dependency drift before it becomes a timeout | p95 above your SLO for 10 minutes |
| Queue depth / dead-letter count (on the source, not the app) | The real backlog signal — an app processing nothing looks healthy | Dead-letter count > 0; oldest message age > N |
| HTTP 5xx | Availability | Any sustained rate |
| Instance count pinned at maximum | You've hit the ceiling and are now queueing | Sustained at max |
| Health check endpoint | Distinguishes "no traffic" from "broken" | Failed health check |
The dead-letter alert deserves emphasis: the most dangerous Functions failure is the silent one, where triggers stop firing and every dashboard on the app itself looks fine because nothing is erroring. Alert on the source's backlog, not just the app's errors.
The KQL query you'll type most — recent failures with their exception, grouped by function:
requests
| where timestamp > ago(1h)
| where success == false
| join kind=leftouter (
exceptions
| where timestamp > ago(1h)
| project operation_Id, exceptionType = type, outerMessage
) on operation_Id
| summarize failures = count(), sample = any(outerMessage) by operation_Name, exceptionType
| order by failures desc
And the cold-start question, which is really "how often is a request served by a brand-new instance":
requests
| where timestamp > ago(24h)
| summarize p50 = percentile(duration, 50), p95 = percentile(duration, 95),
p99 = percentile(duration, 99), count() by operation_Name
| order by p99 desc
A p99 that is tens of times the p50 on a low-traffic HTTP function is the signature of cold start, not of a slow dependency.
The activity log answers a different question — who changed this resource and when. Route it to the same workspace; it is what you will actually query during a "it worked yesterday" investigation.
Reliability
Zone redundancy. A function app is regional. Zone redundancy is a property of the plan and is available on Premium and Flex Consumption in regions that support it ⚠️ verify current availability against current Azure docs. Consumption gives you no zonal guarantee. If a zone outage must not take your workload down, this is a plan decision made at design time, not a switch flipped later.
Multi-region means two function apps in two regions with an active/active or active/passive front. The hard parts are never the compute:
- State. The backing storage account is regional. Durable orchestrations do not fail over. Design regional independence into the workflow or accept that in-flight orchestrations are lost.
- Triggers. Two apps both listening to one queue will each take a share — sometimes what you want, sometimes duplicated work. Two apps listening to regional queues is usually cleaner.
- Routing. Front Door or Traffic Manager in front for HTTP; for event-driven work there is often no routing layer at all and failover is a deployment action.
Backup and restore. There's little to back up in the app itself — it's rebuilt from source and a package, which is the point. What genuinely needs a recovery story:
- The backing storage account (Durable history, function keys) — enable soft delete for blobs.
- Key Vault contents — soft delete and purge protection on.
- Function keys, if any integration depends on a specific key value. Keep them in Key Vault so they survive a storage account recreate.
Idempotency is a reliability control, not a code style preference. At-least-once delivery means your function will run twice for the same event. Carry an idempotency key, make the write conditional, and you have removed an entire class of production incident that no amount of platform configuration can address.
The failure drill worth actually running: disable the trigger source (stop the Service Bus queue, or block the storage account), confirm your alerts fire on backlog rather than on error rate, then restore and confirm the backlog drains without duplicating work. That one exercise tests observability, scaling, and idempotency together, and most teams discover at least one gap.
Next: Interview Questions →
← Back to the Azure Functions overview · ← Previous: Integrations