8. Interview Questions
Three tiers, with an answer key under each question. Write your own answer first, then open the block — the gap between the two is the thing worth studying.
Tier 1 — Conceptual
1. What is Azure Functions and what problem does it solve?
Answer
An event-driven serverless compute service: you write a handler, declare a trigger, and Azure runs it, scaling instances up and down — including to zero — without you provisioning servers.
The problem it kills is idle, and more precisely the plumbing tax around small event-driven work. Before it, "when a file lands, make a thumbnail" required a VM, a polling daemon, retry logic, poison-message handling, a process supervisor, and a monthly patch cycle — for thirty lines of real code. The trigger declaration replaces all of that.
The trade you accept in return: the platform must be free to stop running you, which is where cold starts, statelessness, and execution timeouts come from.
2. Explain the resource hierarchy in your own words, up through resource group and subscription.
Answer
A function is a handler with one trigger — and crucially, it is not an ARM resource. Many
functions live inside a function app, which is a Microsoft.Web/sites resource with
kind = functionapp. The app is the deployment unit, the scaling unit, the configuration unit, and
the identity boundary. The app runs on a hosting plan (Microsoft.Web/serverfarms) and depends
on a storage account. All of those sit in a resource group, in a subscription, in a tenant.
The load-bearing detail is that there's no resource ID for an individual function. You cannot assign RBAC to one, scale one independently, or give one its own identity — which is why grouping functions into apps is an architectural decision, not an organisational one.
3. What delivery and durability guarantees do you get, and what does that mean for your code?
Answer
Queue-shaped triggers (Storage queues, Service Bus, Event Hubs) give at-least-once delivery. Retries, host restarts, and scale-in can all cause the same message to be processed twice. Ordering is guaranteed only within an Event Hubs partition or a Service Bus session, and only if concurrency is constrained.
Therefore idempotency is your responsibility. Carry an idempotency key and make writes conditional. Nothing in the platform makes a handler safe to run twice, and non-idempotent handlers are the most common production defect in serverless systems.
Output bindings are also non-transactional: a function writing to two outputs can fail between them.
4. When would you choose Functions over Container Apps or App Service?
Answer
Functions when the work is event-driven, short, and bursty, and the trigger plumbing is most of what you'd otherwise write.
App Service when it's a conventional HTTP application with steady traffic — you want a long-running process, and per-execution pricing buys you nothing.
Container Apps when you have a container, need more than one process, want microservice-style networking, or want KEDA-based scaling on arbitrary sources. It also scales to zero, so the "serverless" argument alone doesn't distinguish them.
Worth adding: Functions and App Service are the same resource type, and Functions can run on Container Apps. The boundaries are product decisions, not hard technical walls.
5. What are you billed for, and what keeps billing when nothing is running?
Answer
On Consumption and Flex Consumption: executions plus GB-seconds (memory × duration), after a monthly free grant. Idle is effectively free — except for any always-ready instances you bought on Flex.
On Premium (EP1–EP3): per instance, per hour, with a minimum of one instance that never goes
to zero. On Dedicated: the App Service Plan bills hourly regardless of activity.
And the meters people forget: Application Insights ingestion (frequently larger than the compute bill), storage transactions (Durable Functions and polling blob triggers are transaction-heavy), private endpoints, and egress.
The classic surprise line item is an idle EP1 plan created for a one-week experiment.
Tier 2 — Technical depth
1. Walk me through what happens internally when an HTTP request hits a cold function app.
Answer
DNS resolves to the shared App Service front end, which terminates TLS and routes by hostname. The
front end looks for a worker assigned to the site; with the app scaled to zero there is none, so the
platform allocates one from a pre-warmed pool and mounts the app content. The Functions host starts,
reads host.json, loads binding extensions, and enumerates functions. For non-in-process languages a
separate language worker process starts and registers over gRPC — this is where your dependency
tree loads and is usually the largest controllable slice of cold start. Input bindings are resolved,
your code runs, output bindings flush on success, and telemetry goes to Application Insights.
Cold start is those four distinct stages — worker allocation, host start, worker + dependency load, and your own first-call initialisation. Knowing which one dominates determines the fix: only the first is addressed by always-ready or pre-warmed instances; the last two are yours.
2. How does it scale, where's the ceiling, and at what scope is that ceiling counted?
Answer
An Azure-managed scale controller — external to your app — monitors the trigger sources (queue depth, oldest message age, unprocessed events per partition, HTTP request pressure) and adjusts the instance count. Target-based scaling computes a target from backlog ÷ per-instance concurrency and jumps there, rather than adding one instance per interval.
Scaling is per app, not per function: every function in the app shares one instance count.
Ceilings, with scope: maximum instances per function app (varies by plan and OS); total capacity per subscription per region, shared with other App Service workloads; parallelism bounded per partition for Event Hubs and Cosmos DB, and per session for Service Bus.
The honest answer to "where's the ceiling" is usually downstream — the database connection pool, the provisioned RU/s, the rate-limited API, or SNAT ports — not the plan.
3. Compare Consumption, Flex Consumption, and Premium. What does moving between them cost you?
Answer
Consumption: scales to zero, cheapest idle, unmitigated cold start, no VNet integration, shortest timeout ceiling.
Flex Consumption: keeps scale-to-zero economics, adds VNet integration and always-ready instances to buy away cold start incrementally, longer configurable timeout. The modern default.
Premium (EP1–EP3): pre-warmed instances remove cold start, full VNet integration, long
timeouts — but a minimum instance count that never reaches zero, billed hourly forever.
Moving is a deployment, not a setting: the app moves to different infrastructure, outbound IPs change, scaling behaviour changes, and Flex Consumption is a different Terraform resource type rather than a SKU change on the plan. Plan for a redeploy and a networking review.
The trap in each direction: Consumption traps you when a private dependency appears; Premium traps you on the bill when nobody is watching.
4. How do you secure a function app with least privilege and no keys or connection strings?
Answer
Inbound: replace function keys with App Service Authentication (Easy Auth) validating Entra ID tokens before your code runs, or front the app with API Management. Function keys are shared secrets with no identity, no expiry, and no per-caller revocation — acceptable for a webhook from a system that can't do better, not as a security model.
Outbound: a system-assigned managed identity on the app, plus least-privilege data-plane
role assignments at the narrowest scope — Storage Blob Data Contributor, Key Vault Secrets User,
Azure Service Bus Data Receiver. For any remaining secret, a Key Vault reference in an app
setting resolved by that identity.
Plus the hardening: httpsOnly, TLS 1.2 minimum, FTPS and basic publishing credentials disabled,
shared_access_key_enabled = false on the backing storage account (with the blob, queue and
table data roles granted, or every trigger stops), publicNetworkAccess disabled with private
endpoints — remembering that SCM is a separate sub-resource.
5. Control plane vs. data plane for Functions — which RBAC roles govern which, and what's the classic mistake?
Answer
There are effectively three planes. The control plane is ARM (management.azure.com): create,
update, delete, app settings, restart, slot swap, and key listing, governed by Azure RBAC
(Contributor, Website Contributor). The data plane is myapp.azurewebsites.net: invoking
your functions, governed by function/host keys — or by Entra ID if you enable Easy Auth. Azure
RBAC does not govern invocation by default. The SCM/Kudu plane
(myapp.scm.azurewebsites.net) is a third door for deployment and file access.
The classic mistake: assuming control-plane Contributor doesn't imply data-plane access. It does —
Contributor can list the function keys, and for a key-protected app that is equivalent to full
invocation rights. If you need to prevent that, write a custom role excluding the listkeys actions.
The second mistake is forgetting SCM exists: locking the app's public access while leaving the deployment endpoint exposed.
6. Which changes force ARM to replace a function app rather than update it, and what does that cost you?
Answer
Replacement is forced by changing the app's name, resource group, or region; by changing the plan's OS type (Linux ↔ Windows), which recreates the plan and everything on it; and by changing the storage account name.
That last one is the expensive one. If the storage account is AzureWebJobsStorage, replacing it
orphans the Durable Functions orchestration history and the function keys — so in-flight
workflows are lost and every caller holding a key breaks at once.
Practically: read the terraform plan output and stop at any # forces replacement next to a plan
or storage resource. And keep function keys in Key Vault so they survive a storage recreate.
7. Deployment/IaC: how do you structure Terraform for a function app, and what does Terraform not own?
Answer
A parameterised module — main.tf / variables.tf / outputs.tf — owning the resource group, plan,
storage account, function app, managed identity, role assignments, Log Analytics workspace,
Application Insights, and the diagnostic setting. State in an azurerm backend (Azure Storage, using
native blob leases for locking, with use_azuread_auth), directory- or backend-file-per-environment
rather than workspaces for anything with a prod. The provider features {} block matters — the Key
Vault purge behaviour and prevent_deletion_if_contains_resources in particular.
What Terraform must not own: the code package. Infrastructure and code have different
lifecycles and different pipelines. Put WEBSITE_RUN_FROM_PACKAGE and the Application Insights
hidden-link tag under lifecycle { ignore_changes = [...] }, or every infrastructure apply will
fight your last release.
Also worth saying: use storage_uses_managed_identity so the storage key never lands in state — but
remember the three data-plane role assignments (blob, queue, table) that this requires.
Tier 3 — Scenario / design
1. "Our HTTP function's p99 is 8 seconds but p50 is 120 ms, and the database keeps returning 429s under load. Diagnose and fix."
Answer
Two independent problems that people conflate.
The p99 spread is cold start, not a slow dependency — a p99 tens of times the p50 on a low-traffic HTTP function is its signature. Confirm with a percentile query in Application Insights and check whether the slow requests correlate with new instances. Fix in order: hoist SDK client construction to static/module scope (the most common cause and free to fix), reduce package size and lazy-load dependencies, deploy run-from-package, and only then buy always-ready instances on Flex or pre-warmed instances on Premium. Timer-ping warming is a workaround, not a design.
The 429s are the opposite problem — the platform scaling out successfully into a database that
can't take it. A bigger plan makes this worse. Fix by capping pressure: per-instance concurrency in
host.json (maxConcurrentRequests), a maximum scale-out limit on the app, connection pooling with
a reused client, and — the durable answer — a Service Bus queue between ingress and the write path so
a burst becomes a drain. Serverless compute in front of a non-serverless datastore is the single most
common architectural failure in this service.
2. "Design an order-processing system on Functions that must handle a 10× Black Friday spike, guarantee no lost orders, and keep customer data in the EU."
Answer
Ingress: HTTP-triggered function behind Front Door with WAF, Easy Auth or APIM for authentication. Its only job is to validate and enqueue — never to do the work synchronously. That decouples the spike from the processing.
Buffer: Service Bus queue (Premium tier for predictable throughput and VNet support). The queue
absorbs 10× and the drain rate is controlled by maxConcurrentCalls and the app's maximum scale-out
— so downstream systems see a bounded rate no matter how large the spike.
Processing: a separate function app on Flex Consumption or Premium, sized so it can reach the database without cold-start pain. Idempotent handlers keyed on order ID, because Service Bus is at-least-once. Failures dead-letter automatically; alert on dead-letter depth and oldest-message age, not just on error rate.
Orchestration: if the order flow has steps with waits (payment, fraud check, fulfilment), Durable Functions fan-out/fan-in with checkpointing rather than one long function.
No lost orders: durable queue with dead-lettering, idempotent writes, and alerting on backlog. The ingress function must not acknowledge until the message is enqueued.
Data residency: deploy everything into EU regions; pin the storage account, Service Bus namespace, Cosmos/SQL, and Log Analytics workspace to EU. Note that Application Insights and Log Analytics hold telemetry that may contain personal data — their region matters as much as the database's. Enforce region with Azure Policy at the management-group level rather than trusting review. For resilience, a second EU region with its own regional queue and app, fronted by Front Door — accepting that in-flight Durable orchestrations do not fail over.
Capacity: check instance quota per subscription per region ahead of the event and raise it early — it's shared with other App Service workloads, and it's a soft limit you can raise, but not on the day.
3. "A prod deployment failed halfway. What's your rollback and blast-radius reasoning — and what would a re-run in complete deployment mode do?"
Answer
First, separate which deployment failed. If it was the code, and you deployed to a staging slot and swapped, roll back by swapping again — near-atomic, the previous version is still warm, recovery in seconds. That is the strongest argument for using slots at all. Watch that app settings swap too unless marked as deployment-slot (sticky) settings. If there's no slot, redeploy the previous package — slower, and you pay a cold start.
If it was the infrastructure, re-apply the previous commit. But read the plan first: if it shows
# forces replacement on the plan or the storage account, a "rollback" will recreate the storage
account and take your Durable history and function keys with it. That's a bigger outage than the
failed deploy.
Then the Azure-specific traps that make a rollback fail confusingly: a soft-deleted Key Vault
still holds its name and blocks recreation, and with purge protection on it can't be purged until
retention expires — the error looks like a naming bug. Resource locks (CanNotDelete) make an
apply fail with what reads exactly like a missing RBAC role. Check for locks before debugging
permissions.
Complete mode would be the catastrophic version: az deployment group create --mode Complete
deletes every resource in the resource group that isn't in the template. For a function app that
means the backing storage account — Durable state, function keys — can vanish because someone wanted
a clean deployment. Incremental is the default and the safe one; complete is defensible only in a
resource group entirely owned by that template.
4. "Someone changed app settings by hand in the portal three weeks ago. How do you find out, and how do you get back to a clean terraform plan?"
Answer
Detect: a scheduled terraform plan in CI — nightly per environment, failing on a non-empty diff —
is the control that would have caught this on day one, and it costs almost nothing. Alongside it,
Azure Policy compliance state catches the classes you care about most regardless of tool, and
az deployment group what-if covers the Bicep path.
Attribute: the activity log answers "who and when", which is the question that actually gets asked. Route it to Log Analytics and query it in KQL; three weeks is beyond the default portal retention, which is exactly why the routing matters.
Reconcile: decide whether the change was legitimate. If it was, codify it — put the setting in the
module and apply, so the desired state now matches reality. If it wasn't, terraform apply reverts
it. If the resource was created out of band entirely, terraform import (or an import block)
brings it under management rather than recreating it.
Prevent recurrence: tighten prod RBAC so humans have Reader and the pipeline identity has
Contributor; use Azure Policy to deny the specific misconfigurations; and — the Functions-specific
part — be explicit about which app settings Terraform owns. Some settings are written
automatically by the platform and by deployment tooling, so put those under ignore_changes.
Otherwise your drift alert becomes noise and the team stops reading it, which is worse than not
having one.
Next: Glossary & Cheatsheet →
← Back to the Azure Functions overview · ← Previous: Production