Recently, I noticed another Azure architecture infographic on LinkedIn. Four boxes, left to right: clients, API Management, Function App, backend services. Underneath it, a tidy line. Function App handles the code, API Management handles the control. Together they deliver powerful and secure APIs.

(Source: LinkedIn post)
Nothing in that diagram is wrong. However, it’s not the whole story.
I have reviewed this pattern several times, in reference architectures, in project designs, and in my own work. The four boxes are always right and the design behind them is often not. The gap sits in what the arrows imply rather than in what the boxes say. So let us redraw it, and then walk through the six things the four box version quietly leaves out.

What the original gets right
Credit where it belongs. The separation of concerns in that infographic is sound, and plenty of teams still get it backwards.
API Management owns the contract. It publishes the API, applies policy, meters consumption, and gives consumers something stable to build against. Azure Functions owns the work. It runs your business logic, scales with demand, and bills for execution rather than for uptime.
That split matters because the alternative is worse. Teams that skip the gateway end up implementing authentication, throttling, and versioning inside every function, in slightly different ways, maintained by whoever touched it last. I wrote about a related boundary problem in Azure Functions, Logic Apps, and Power Automate: choosing the right tool, and the same principle applies here. The value of a gateway is not the features it lists. The value is the code it lets you delete.
So the boxes are right. Now for the arrows.
Gap one: a gateway is not a firewall
The original diagram lists “Security (OAuth, API Key)” inside the API Management box and leaves it there. That single bullet does a lot of quiet work, because it invites you to treat the gateway as your perimeter.
API Management applies policy. It validates tokens, enforces quotas, transforms payloads, and rejects malformed requests against a schema. It does not run an OWASP rule set, and it is not designed to absorb a volumetric attack aimed at your public hostname.
For anything internet facing, put Azure Front Door Premium or Application Gateway in front, with a WAF policy attached. Then lock the origin so the gateway only accepts traffic that arrived through the edge. Otherwise you have bought a policy engine and called it a perimeter.
This is also the cheapest gap to close, which makes it the most annoying one to find missing during a penetration test.
Gap two: subscription keys meter, tokens authenticate
Here is the conflation that causes the most damage in practice. A subscription key and an access token appear side by side in most diagrams, as if they were two ways of doing the same thing.
They are not. A subscription key identifies a product. It answers the question “which consumer agreement does this call belong to”, which is a billing and quota question. It does not tell you who the caller is, it travels in a header that ends up in scripts and log files, and it is shared across everyone using that product.
Authentication happens in the validate-jwt policy. That is where you check the signature, the audience, the issuer, and the claims that decide whether this particular caller may create an order.
<validate-jwt header-name="Authorization" failed-validation-httpcode="401"> <openid-config url="https://login.microsoftonline.com/{{entra-tenant-id}}/v2.0/.well-known/openid-configuration" /> <audiences> <audience>{{orders-api-audience}}</audience> </audiences> <required-claims> <claim name="roles" match="any"> <value>Orders.Write</value> </claim> </required-claims></validate-jwt>
Once that is in place, the rate limit should follow the same identity. Throttling by IP address punishes everyone behind a corporate NAT and protects you from nobody who has a laptop and patience. Throttling by a claim from the validated token gives you a quota per caller, which is what the consumer actually agreed to.
<rate-limit-by-key calls="60" renewal-period="60" counter-key="@(context.Request.Headers.GetValueOrDefault("Authorization","").AsJwt()?.Claims.GetValueOrDefault("oid", "anonymous"))" />
Gap three: the arrow nobody draws
Now for the one that matters most.
Every version of this diagram shows one arrow into the Function App. That arrow implies the gateway is the way in. It is not. It is a way in.
An HTTP triggered function sits on a public hostname by default. Anyone holding that hostname and a function key can call it directly, and that call skips the WAF, the token validation, the rate limit, the audit trail, and every policy you carefully wrote. The gateway becomes a convention rather than a control, and conventions do not survive contact with an incident.

Teams close this in three stages, usually in this order.
Layer one, the function key. Store the host key as a secret named value in API Management, backed by Key Vault, and let the backend inject it as x-functions-key. The key never appears in your policy file or your repository. This stops crawlers and casual discovery. It does not stop anyone who has ever seen the key, and keys have a way of ending up in Postman collections, log files, and support tickets.
Layer two, token validation on the function itself. Turn on the built-in authentication, point it at the app registration that represents your API, and an untokened call now fails at the platform before your code runs. Better. Still not closed, because a valid token replayed straight at the function hostname bypasses everything the gateway added on top.
Layer three, take it off the internet. Set publicNetworkAccess to Disabled, put a private endpoint in front of the app, and integrate the gateway into the same virtual network. The hostname stops resolving from outside. Now there is genuinely one route in.
resource functionApp 'Microsoft.Web/sites@2023-12-01' = { properties: { publicNetworkAccess: networkIsolation ? 'Disabled' : 'Enabled' virtualNetworkSubnetId: networkIsolation ? functionSubnetId : null }}
Layer three is the only one that turns your architecture diagram into a statement about reality. It also has a prerequisite that catches people out, which is the next gap.
Gap four: the tier decides the architecture
Private networking is not a checkbox you add at the end. It is gated by the API Management tier and by the Functions hosting plan, and those two choices constrain everything else in the design.
The v2 tiers changed the arithmetic here. Outbound virtual network integration used to mean Premium, which put private connectivity out of reach for a lot of internal APIs. Standard v2 brought it within reach of an ordinary project budget. On the compute side, the Flex Consumption plan brought virtual network integration to a consumption billing model, which used to mean choosing between cost and connectivity.
The same is true of time. Both the gateway and the function cap how long a request may run, and those caps differ by tier and plan. If your design assumes a two minute synchronous call, you have made a tier decision without realising it.
Pick the tier and the hosting plan before you pick the features, and verify the current limits on Microsoft Learn rather than trusting a diagram. Both sides of this moved several times during 2025 and 2026, and anything I write here has a shelf life.
Gap five: the dashed response arrow assumes synchronous
Look at the original infographic again. The response arrow is dashed and runs straight back to the client. It quietly assumes every call finishes inside the request.
Plenty do not. Bulk imports, report generation, anything that fans out to a slow downstream system. The instinct is to raise the timeout, first on the function, then on the gateway, until the whole chain waits for the slowest possible caller. That is not a fix. It is a queue with worse ergonomics, and it fails under load rather than under test.
The pattern that works is the asynchronous HTTP API. Accept the request, start the work, and answer immediately.
var instanceId = await durableClient.ScheduleNewOrchestrationInstanceAsync( nameof(BulkImportOrchestrator), orders);var response = request.CreateResponse(HttpStatusCode.Accepted);response.Headers.Add("Location", $"{request.PublicBaseUrl()}/bulk/{instanceId}");response.Headers.Add("Retry-After", "5");
One detail deserves attention, because it connects back to gap three. Durable Functions ships a helper called CreateCheckStatusResponse that builds the polling URLs for you. Those URLs point at the function hostname. That leaks your backend to every caller, and it breaks the moment you disable public network access.
So build the status URL from a setting that holds the gateway address instead. Callers should never learn the name of your function app, and they certainly should not be given it in a header.
Gap six: one request, two telemetry stores
Both boxes in the diagram write to Application Insights, usually to two separate resources with two separate sampling configurations. Neither picture shows that, because telemetry is drawn as a property rather than as a system.
The result shows up during your first real incident. You have a gateway trace that ends at the backend call and a function trace that starts somewhere in the middle, and no reliable way to join them. Then somebody discovers that the gateway sampled at one rate and the function at another, so half the pairs do not exist at all.
Two habits fix this. Set a correlation id at the gateway when the caller did not supply one, and propagate it through the function into every log line.
<set-variable name="correlationId" value="@(context.Request.Headers.GetValueOrDefault("x-correlation-id", context.RequestId.ToString()))" /><set-header name="x-correlation-id" exists-action="override"> <value>@((string)context.Variables["correlationId"])</value></set-header>
Then align the sampling on both sides, and write the query that joins them before you need it rather than during an outage. I made a similar argument about governed telemetry in the Foundry Citadel Platform series, where an SDK routed its calls around the gateway entirely and the only reason we noticed was that the traces did not add up.
Where this is the wrong answer
Everything above assumes you need API Management. Often you do not.
A gateway earns its place when there is a portfolio to govern. Several APIs, consumers outside your own team, versions that have to coexist, quota that differs per consumer, one place where policy and audit live. Under those conditions API Management pays for itself in the coordination it removes.
For a single internal API with one consumer, it is a monthly bill wrapped around a proxy. Built-in authentication with Entra ID on the function, a private endpoint if it is internal, and you are done. Add the gateway when the second consumer actually appears, because that is when versioning and per consumer quota start to matter.
There is also a middle answer that gets skipped. One public API that needs a WAF but has no consumer lifecycle should sit behind Front Door and nothing else. You get the perimeter without buying governance you are not using yet.

A sample you can deploy
I put the whole thing in a repository, because policy fragments in a blog post are easy to agree with and harder to run.
One azd up deploys API Management Standard v2, a Flex Consumption function app on .NET 8 isolated, a storage account with shared key access disabled, and a shared Application Insights resource. The Orders API has a synchronous endpoint, an asynchronous bulk import built on Durable Functions, and a health endpoint the gateway can probe.
The three layers from gap three are deployment switches rather than prose:
| Setting | What it turns on |
|---|---|
| default | Layer one, function key held as a secret named value |
ENTRA_TENANT_ID and API_AUDIENCE | Layer two, validate-jwt and quota by oid claim |
NETWORK_ISOLATION=true | Layer three, private endpoint and public access disabled |
Deploy it with the default settings, then call the function hostname directly and watch it answer. That single curl makes the argument better than this whole post does.
Closing
The four box diagram is a good summary and a bad specification. Function App handles the code, API Management handles the contract, and neither of them handles the network. That last part is where these designs usually fail, and it is the part no infographic ever draws.


















