<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" version="2.0">
  <channel>
    <title>Apps on Azure Blog articles</title>
    <link>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/bg-p/AppsonAzureBlog</link>
    <description>Apps on Azure Blog articles</description>
    <pubDate>Tue, 21 Jul 2026 07:52:27 GMT</pubDate>
    <dc:creator>AppsonAzureBlog</dc:creator>
    <dc:date>2026-07-21T07:52:27Z</dc:date>
    <item>
      <title>Microsoft Foundry Now Has an AI Gateway Control Plane — What Changes for App Service</title>
      <link>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/microsoft-foundry-now-has-an-ai-gateway-control-plane-what/ba-p/4538320</link>
      <description>&lt;P&gt;In May, I published a &lt;A class="lia-internal-link lia-internal-url lia-internal-url-content-type-blog" href="https://techcommunity.microsoft.com/blog/appsonazureblog/you-can-build-a-framework-agnostic-ai-gateway-on-azure-app-service-%E2%80%94-heres-how/4522004" target="_blank" rel="noopener" data-lia-auto-title="runnable sample that put Azure API Management in front of an AI agent on Azure App Service" data-lia-auto-title-active="0"&gt;runnable sample that put Azure API Management in front of an AI agent on Azure App Service&lt;/A&gt;. APIM handled token limits, semantic caching, token metrics, and access to the model. The point was simple: keep the agent framework interchangeable and put the production controls at the gateway.&lt;/P&gt;
&lt;P&gt;A recent Apps on Azure post, &lt;A class="lia-internal-link lia-internal-url lia-internal-url-content-type-blog" href="https://techcommunity.microsoft.com/blog/appsonazureblog/from-ai-adoption-to-ai-governance---using-apim-as-the-gateway-for-azure-ai-found/4536247" target="_blank" rel="noopener" data-lia-auto-title="From AI Adoption to AI Governance" data-lia-auto-title-active="0"&gt;From AI Adoption to AI Governance&lt;/A&gt;, arrived at the same architectural conclusion from a platform-governance angle. It uses APIM to turn model identity and token consumption into a per-model chargeback signal for Microsoft Foundry workloads.&lt;/P&gt;
&lt;P&gt;That is useful validation, but the more interesting update is in the product itself: &lt;STRONG&gt;Microsoft Foundry can now create or associate an APIM-based AI Gateway directly from its Admin console.&lt;/STRONG&gt; The data path is still APIM. What changed is who can configure and govern it.&lt;/P&gt;
&lt;P&gt;This post explains what that means for an App Service-hosted agent, what Foundry now manages, what still belongs in APIM, and what I would change in the sample we built earlier.&lt;/P&gt;
&lt;H2&gt;The short version: same gateway, new control plane&lt;/H2&gt;
&lt;P&gt;The architecture is still familiar:&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Client -&amp;gt; App Service agent -&amp;gt; APIM AI Gateway -&amp;gt; Foundry model&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;APIM remains the traffic gateway. It authenticates callers, applies policies, forwards requests, and emits telemetry. App Service remains the application runtime for the agent and its APIs.&lt;/P&gt;
&lt;P&gt;The new piece is the Foundry control plane. From &lt;STRONG&gt;Operate &amp;gt; Admin console &amp;gt; AI Gateway&lt;/STRONG&gt;, a platform team can now:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Create a new APIM instance or associate a supported existing instance.&lt;/LI&gt;
&lt;LI&gt;Enable the gateway for individual Foundry projects.&lt;/LI&gt;
&lt;LI&gt;Apply independent token limits to projects sharing the gateway.&lt;/LI&gt;
&lt;LI&gt;Verify gateway traffic and inspect logs.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Once the gateway is enabled, separate Foundry experiences can also register custom agents and govern supported MCP tools. Those actions do not happen in the AI Gateway tab itself, but the gateway provides their governed traffic path.&lt;/P&gt;
&lt;P&gt;That is a meaningful shift. In the original sample, the infrastructure owner created APIM, imported the model API, deployed policy XML, and wired the agent to the gateway. Foundry does not remove APIM or replace those advanced capabilities. It gives Foundry administrators a first-party path into the same architecture.&lt;/P&gt;
&lt;H2&gt;What Foundry now owns&lt;/H2&gt;
&lt;H3&gt;Project onboarding&lt;/H3&gt;
&lt;P&gt;An AI Gateway is associated with a Foundry resource, then enabled for projects inside it. New projects can inherit the gateway automatically. Existing projects must be added explicitly.&lt;/P&gt;
&lt;P&gt;This creates a useful governance boundary: multiple teams can share the same APIM instance without sharing one undifferentiated token pool. Each project can receive its own token ceiling. If a project exceeds its configured limit, the gateway returns &lt;CODE&gt;429 Too Many Requests&lt;/CODE&gt; while other projects continue using their allocations.&lt;/P&gt;
&lt;H3&gt;Foundry-level limits&lt;/H3&gt;
&lt;P&gt;Our earlier sample used an APIM token-limit policy keyed by an APIM subscription. That is still a valid pattern, especially when the consumer boundary is an application, business unit, or external customer.&lt;/P&gt;
&lt;P&gt;The Foundry integration adds another natural counter key: the Foundry project. For organizations already organizing models and agents by project, limits can now follow that structure instead of being recreated independently in every client application.&lt;/P&gt;
&lt;H3&gt;Inventory for agents and tools&lt;/H3&gt;
&lt;P&gt;The AI Gateway experience is expanding beyond model endpoints in two distinct ways. Foundry can register custom agents running outside Foundry, including agents that use A2A as their communication protocol. Separately, AI Gateway can govern supported MCP tools. The current tools-governance preview is scoped to MCP; it does not cover every Foundry or OpenAPI tool type.&lt;/P&gt;
&lt;P&gt;That matters for App Service because the runtime does not need to move. An agent can continue running on App Service while participating in Foundry's inventory, and its supported MCP traffic can flow through the gateway.&lt;/P&gt;
&lt;P&gt;These experiences are still preview features, so I would treat them as a control-plane integration to evaluate, not a reason to redesign a working production agent.&lt;/P&gt;
&lt;H2&gt;What still belongs in APIM&lt;/H2&gt;
&lt;P&gt;Foundry makes the common path easier. It does not make the full APIM surface unnecessary.&lt;/P&gt;
&lt;P&gt;I would still use APIM directly for:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Custom policy expressions and organization-specific headers or claims.&lt;/LI&gt;
&lt;LI&gt;Semantic caching backed by Azure Managed Redis.&lt;/LI&gt;
&lt;LI&gt;Backend pools, priority routing, and circuit breakers.&lt;/LI&gt;
&lt;LI&gt;Private networking, multi-region gateways, and advanced topology decisions.&lt;/LI&gt;
&lt;LI&gt;Custom metrics and dimensions needed for internal chargeback.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;The clean mental model is: &lt;STRONG&gt;Foundry manages the AI assets and common project guardrails; APIM manages the traffic contract and advanced gateway behavior.&lt;/STRONG&gt;&lt;/P&gt;
&lt;H2&gt;Applying this to the App Service sample&lt;/H2&gt;
&lt;P&gt;The existing &lt;A class="lia-external-url" href="https://github.com/seligj95/app-service-ai-gateway-mcp-apim-python" target="_blank" rel="noopener"&gt;framework-agnostic AI Gateway sample&lt;/A&gt; hosts a Microsoft Agent Framework agent and an MCP server on one Linux App Service. The agent reaches its model through APIM, which applies token limits, semantic caching, token metrics, and managed-identity authentication.&lt;/P&gt;
&lt;P&gt;Most of that architecture should stay exactly as it is.&lt;/P&gt;
&lt;H3&gt;1. Keep the App Service application unchanged&lt;/H3&gt;
&lt;P&gt;The agent should continue calling a stable gateway endpoint. It should not need to understand whether APIM was provisioned by Bicep, associated in Foundry, or managed by a central platform team. That separation was the reason to introduce the gateway in the first place.&lt;/P&gt;
&lt;H3&gt;2. Choose the APIM ownership model deliberately&lt;/H3&gt;
&lt;P&gt;For a new proof of concept, Foundry can create a Basic v2 APIM instance. For production, Microsoft recommends evaluating Standard v2 or Premium v2 based on throughput and networking requirements.&lt;/P&gt;
&lt;P&gt;For an existing enterprise gateway, use the Foundry option to associate an APIM instance only after confirming that it meets the eligibility requirements: it must be in the same Microsoft Entra tenant and Azure subscription as the Foundry resource, you need the &lt;STRONG&gt;API Management Service Contributor&lt;/STRONG&gt; role (or Owner), and the instance must use a supported v2 tier.&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;STRONG&gt;Important migration detail:&lt;/STRONG&gt; the original sample defaults to the APIM Developer tier. That instance will not appear in Foundry's &lt;EM&gt;Use existing APIM&lt;/EM&gt; list because the direct integration currently requires a v2 tier. Do not assume an existing classic-tier gateway can simply be attached.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;H3&gt;3. Add existing projects explicitly&lt;/H3&gt;
&lt;P&gt;Creating or associating the gateway at the Foundry resource level does not automatically enable every existing project. Add each existing project to the gateway, then verify its status is &lt;STRONG&gt;Enabled&lt;/STRONG&gt;.&lt;/P&gt;
&lt;H3&gt;4. Decide where each limit belongs&lt;/H3&gt;
&lt;P&gt;Avoid layering limits without a clear ownership model. A Foundry project limit, an APIM subscription limit, and a model deployment TPM limit answer different questions:&lt;/P&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;&lt;table border="1" style="border-width: 1px;"&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Control&lt;/th&gt;&lt;th&gt;Best boundary&lt;/th&gt;&lt;th&gt;Question it answers&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Foundry project limit&lt;/td&gt;&lt;td&gt;Team or workload project&lt;/td&gt;&lt;td&gt;How much shared capacity can this project consume?&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;APIM policy limit&lt;/td&gt;&lt;td&gt;Subscription, user, tenant, or application&lt;/td&gt;&lt;td&gt;How much can this specific consumer use?&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Model deployment quota&lt;/td&gt;&lt;td&gt;Backend deployment&lt;/td&gt;&lt;td&gt;What capacity exists at the model endpoint?&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;colgroup&gt;&lt;col style="width: 33.33%" /&gt;&lt;col style="width: 33.33%" /&gt;&lt;col style="width: 33.33%" /&gt;&lt;/colgroup&gt;&lt;/table&gt;&lt;/DIV&gt;
&lt;P&gt;Use all three when the boundaries are intentional. Otherwise, operators end up debugging a &lt;CODE&gt;429&lt;/CODE&gt; without knowing which layer produced it.&lt;/P&gt;
&lt;H3&gt;5. Verify the data path&lt;/H3&gt;
&lt;P&gt;After enabling a project, make a model request and confirm that the APIM request metric increments. Microsoft also recommends checking the &lt;CODE&gt;GatewayLogs&lt;/CODE&gt; table for a successful response and an API name matching the AI Gateway.&lt;/P&gt;
&lt;P&gt;Then test the failure path. Set a deliberately small project limit, exceed it, and confirm that the gateway returns &lt;CODE&gt;429&lt;/CODE&gt;. A governance feature is not finished until the team has observed both the allowed and denied behavior.&lt;/P&gt;
&lt;H2&gt;Which path should you choose?&lt;/H2&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;&lt;table border="1" style="border-width: 1px;"&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Scenario&lt;/th&gt;&lt;th&gt;Recommended approach&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;New Foundry proof of concept&lt;/td&gt;&lt;td&gt;Create the AI Gateway from Foundry and start with project-level limits.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Existing v2 enterprise APIM&lt;/td&gt;&lt;td&gt;Associate the existing instance and preserve central networking and policy ownership.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Existing classic-tier APIM&lt;/td&gt;&lt;td&gt;Keep the working gateway or plan a deliberate v2 migration; it cannot be selected directly today.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Advanced routing or custom policy needs&lt;/td&gt;&lt;td&gt;Use Foundry for inventory and common guardrails, then manage the advanced behavior in APIM.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Strict isolation between project groups&lt;/td&gt;&lt;td&gt;Use separate Foundry resources and separate gateways rather than assuming one gateway per project.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;colgroup&gt;&lt;col style="width: 50.00%" /&gt;&lt;col style="width: 50.00%" /&gt;&lt;/colgroup&gt;&lt;/table&gt;&lt;/DIV&gt;
&lt;H2&gt;The architecture was right; the experience caught up&lt;/H2&gt;
&lt;P&gt;The most important takeaway is not that everyone should rebuild an AI Gateway in a new portal. It is that the composable architecture now has a more accessible control plane.&lt;/P&gt;
&lt;P&gt;App Service still runs the agent. APIM still governs the traffic. Foundry now gives model and platform owners a direct way to connect projects, allocate capacity, and bring agents and tools into the same governance view.&lt;/P&gt;
&lt;P&gt;If you already built the earlier sample, the application boundary does not need to change. Evaluate the Foundry integration, decide whether its project model matches your organization, and check the APIM tier before planning any migration. The gateway remains the contribution; Foundry now makes it easier to operate.&lt;/P&gt;
&lt;H2&gt;Resources&lt;/H2&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;A class="lia-internal-link lia-internal-url lia-internal-url-content-type-blog" href="https://techcommunity.microsoft.com/blog/appsonazureblog/you-can-build-a-framework-agnostic-ai-gateway-on-azure-app-service-%E2%80%94-heres-how/4522004" target="_blank" rel="noopener" data-lia-auto-title="You can build a framework-agnostic AI Gateway on Azure App Service" data-lia-auto-title-active="0"&gt;You can build a framework-agnostic AI Gateway on Azure App Service&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A class="lia-internal-link lia-internal-url lia-internal-url-content-type-blog" href="https://techcommunity.microsoft.com/blog/appsonazureblog/from-ai-adoption-to-ai-governance---using-apim-as-the-gateway-for-azure-ai-found/4536247" target="_blank" rel="noopener" data-lia-auto-title="From AI Adoption to AI Governance" data-lia-auto-title-active="0"&gt;From AI Adoption to AI Governance&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/foundry/configuration/enable-ai-api-management-gateway-portal" target="_blank" rel="noopener"&gt;Configure AI Gateway in your Foundry resources&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/api-management/genai-gateway-capabilities" target="_blank" rel="noopener"&gt;AI Gateway capabilities in Azure API Management&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A class="lia-external-url" href="https://github.com/seligj95/app-service-ai-gateway-mcp-apim-python" target="_blank" rel="noopener"&gt;App Service AI Gateway sample&lt;/A&gt;&lt;/LI&gt;
&lt;/UL&gt;</description>
      <pubDate>Fri, 17 Jul 2026 17:04:16 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/microsoft-foundry-now-has-an-ai-gateway-control-plane-what/ba-p/4538320</guid>
      <dc:creator>jordanselig</dc:creator>
      <dc:date>2026-07-17T17:04:16Z</dc:date>
    </item>
    <item>
      <title>MCP Enterprise Authorization Is Here — What Entra and App Service Can Do Today</title>
      <link>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/mcp-enterprise-authorization-is-here-what-entra-and-app-service/ba-p/4537433</link>
      <description>&lt;P&gt;A few weeks ago I wrote about what the latest MCP changes meant for scaling on Azure App Service. This time, the protocol change is about a different kind of scale: how an enterprise connects hundreds or thousands of employees to MCP servers without making every person authorize every server one at a time.&lt;/P&gt;
&lt;P&gt;&lt;A class="lia-external-url" href="https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization" target="_blank" rel="noopener"&gt;Enterprise-Managed Authorization (EMA)&lt;/A&gt; is now a stable MCP extension. It makes the organization's identity provider the policy decision point, replaces repeated server-by-server browser prompts with an identity assertion grant, and gives security teams a central place to grant and revoke access.&lt;/P&gt;
&lt;P&gt;That sounds a lot like Microsoft Entra preauthorization and App Service Authentication. But while building the sample for this post, I found an important distinction:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;A centrally governed OAuth experience is not automatically the EMA protocol.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;P&gt;App Service and Entra can give you a strongly governed MCP endpoint today. The full EMA flow additionally requires the enterprise identity provider to issue an Identity Assertion JWT Authorization Grant, or ID-JAG, and the MCP authorization server to exchange it. Same goal, related building blocks, different wire protocol.&lt;/P&gt;
&lt;P&gt;So I built both.&lt;/P&gt;
&lt;H2&gt;Why the normal OAuth flow gets expensive&lt;/H2&gt;
&lt;P&gt;Standard MCP authorization is user-driven. A client discovers an MCP server, sends the user through that server's authorization flow, receives an access token, and repeats the process for the next server.&lt;/P&gt;
&lt;P&gt;That is a good property for consumer scenarios. The user chooses which application can reach which data.&lt;/P&gt;
&lt;P&gt;At enterprise scale, the same pattern becomes an authorization tax:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Every employee has to connect every approved MCP server.&lt;/LI&gt;
&lt;LI&gt;Consent and account-selection prompts create inconsistent setup.&lt;/LI&gt;
&lt;LI&gt;Security teams have to reason about access across many separate authorization relationships.&lt;/LI&gt;
&lt;LI&gt;Offboarding and policy changes are harder to apply from one control plane.&lt;/LI&gt;
&lt;LI&gt;Work and personal identities can get mixed together at the worst possible boundary: the tool that can act on a user's behalf.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;EMA moves the authoritative decision back to the enterprise IdP.&lt;/P&gt;
&lt;H2&gt;What EMA changes on the wire&lt;/H2&gt;
&lt;P&gt;The user still signs in to the MCP host with the corporate identity provider. The difference comes when the client needs an access token for an MCP server.&lt;/P&gt;
&lt;OL&gt;
&lt;LI&gt;The MCP client exchanges the user's identity assertion at the enterprise IdP for an ID-JAG. The stable extension uses OAuth token exchange semantics from &lt;A class="lia-external-url" href="https://datatracker.ietf.org/doc/html/rfc8693" target="_blank" rel="noopener"&gt;RFC 8693&lt;/A&gt;.&lt;/LI&gt;
&lt;LI&gt;The IdP evaluates enterprise policy for the requested client, MCP resource, user, and scopes.&lt;/LI&gt;
&lt;LI&gt;If policy allows access, the IdP issues a short-lived, audience-bound ID-JAG.&lt;/LI&gt;
&lt;LI&gt;The client presents that JWT to the MCP authorization server using the JWT bearer grant from &lt;A class="lia-external-url" href="https://datatracker.ietf.org/doc/html/rfc7523" target="_blank" rel="noopener"&gt;RFC 7523&lt;/A&gt;.&lt;/LI&gt;
&lt;LI&gt;The authorization server validates the assertion and returns the resource access token the client uses for MCP calls.&lt;/LI&gt;
&lt;/OL&gt;
&lt;P&gt;&lt;IMG src="https://raw.githubusercontent.com/seligj95/app-service-ema-mcp/main/assets/authorization-paths.png" alt="Standard Entra OAuth on App Service compared with the EMA ID-JAG exchange" /&gt;&lt;/P&gt;
&lt;P&gt;The user never goes through a second browser flow at the MCP authorization server. The enterprise IdP already made the decision.&lt;/P&gt;
&lt;P&gt;That gives the organization three useful properties:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Authorize once, inherit everywhere.&lt;/STRONG&gt; Admins enable approved servers and users receive access according to their existing groups and roles.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Central policy and revocation.&lt;/STRONG&gt; Conditional access, employment state, device requirements, and other policy remain in the IdP.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;A cleaner identity boundary.&lt;/STRONG&gt; The enterprise login is the source of authority instead of an account picker at every MCP server.&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2&gt;What Entra and App Service provide today&lt;/H2&gt;
&lt;P&gt;Azure App Service already has a very good home for an enterprise MCP resource server: &lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/app-service/configure-authentication-mcp" target="_blank" rel="noopener"&gt;App Service Authentication&lt;/A&gt; sits in front of the application and validates Microsoft Entra access tokens before a request reaches your Python process. App Service Authentication’s MCP authorization integration and protected resource metadata support are currently in preview.&lt;/P&gt;
&lt;P&gt;In the deployable half of the sample, App Service:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;publishes OAuth protected resource metadata for MCP discovery;&lt;/LI&gt;
&lt;LI&gt;requires authentication on &lt;CODE&gt;/mcp&lt;/CODE&gt; and returns &lt;CODE&gt;401&lt;/CODE&gt; to unauthenticated requests;&lt;/LI&gt;
&lt;LI&gt;validates token signature, issuer, audience, and lifetime;&lt;/LI&gt;
&lt;LI&gt;allows only the configured client application IDs;&lt;/LI&gt;
&lt;LI&gt;injects the validated principal into platform-controlled request headers;&lt;/LI&gt;
&lt;LI&gt;leaves only &lt;CODE&gt;/&lt;/CODE&gt; and &lt;CODE&gt;/health&lt;/CODE&gt; public;&lt;/LI&gt;
&lt;LI&gt;sends OpenTelemetry data to Application Insights with managed identity.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;The Entra application exposes &lt;CODE&gt;user_impersonation&lt;/CODE&gt; and preauthorizes Visual Studio Code and Azure CLI. Tenant administrators can then apply Conditional Access to the enterprise application and inspect Entra sign-in logs.&lt;/P&gt;
&lt;P&gt;That is centrally governed OAuth. It removes consent prompts for known clients and puts token validation at the platform edge.&lt;/P&gt;
&lt;P&gt;It is not native EMA because this path does not ask Entra to issue an ID-JAG through RFC 8693, and there is no RFC 7523 assertion exchange at a separate MCP authorization server.&lt;/P&gt;
&lt;P&gt;Here is the practical comparison:&lt;/P&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;&lt;table border="1" style="border-width: 1px;"&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Capability&lt;/th&gt;&lt;th&gt;Entra + App Service path&lt;/th&gt;&lt;th&gt;EMA path&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Central tenant policy&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Protected resource metadata&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Known-client preauthorization&lt;/td&gt;&lt;td&gt;Yes, through Entra preauthorization&lt;/td&gt;&lt;td&gt;Often still required at the IdP and authorization server&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Per-server browser flow&lt;/td&gt;&lt;td&gt;Avoided for preauthorized clients&lt;/td&gt;&lt;td&gt;Avoided by protocol design&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Enterprise IdP issues an ID-JAG&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;MCP authorization server exchanges the ID-JAG&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Included in the sample&lt;/td&gt;&lt;td&gt;Deployable Azure app&lt;/td&gt;&lt;td&gt;Local interoperability lab&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;colgroup&gt;&lt;col style="width: 33.33%" /&gt;&lt;col style="width: 33.33%" /&gt;&lt;col style="width: 33.33%" /&gt;&lt;/colgroup&gt;&lt;/table&gt;&lt;/DIV&gt;
&lt;P&gt;The distinction matters because "no consent prompt" describes a user experience, not a protocol proof.&lt;/P&gt;
&lt;H3&gt;Could you run full EMA on App Service today?&lt;/H3&gt;
&lt;P&gt;Yes. App Service does not prevent you from deploying the complete EMA flow. If your enterprise IdP can issue ID-JAGs, you can implement the resource authorization server and MCP token validation in application code—or point the MCP server at a separate authorization service. App Service then remains the compute platform rather than the authorization boundary.&lt;/P&gt;
&lt;P&gt;That approach replaces Easy Auth’s platform-managed token validation with responsibilities such as token issuance, signing-key management, ID-JAG validation, replay protection, scope enforcement, and audit logging. The remaining prerequisite is still an enterprise IdP and MCP client that support the ID-JAG exchange.&lt;/P&gt;
&lt;H2&gt;I built both paths on purpose&lt;/H2&gt;
&lt;P&gt;The companion repository is &lt;A class="lia-external-url" href="https://github.com/seligj95/app-service-ema-mcp" target="_blank" rel="noopener"&gt;seligj95/app-service-ema-mcp&lt;/A&gt;.&lt;/P&gt;
&lt;P&gt;The Azure path is a FastAPI MCP server running on Linux App Service. It exposes two read-only tools:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;CODE&gt;whoami&lt;/CODE&gt; returns the tenant, client, subject, roles, and scopes that App Service Authentication validated.&lt;/LI&gt;
&lt;LI&gt;&lt;CODE&gt;authorization_model&lt;/CODE&gt; states exactly which authorization model protects the deployment.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;The local path runs four components in one process:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;a stand-in enterprise IdP;&lt;/LI&gt;
&lt;LI&gt;an MCP authorization server;&lt;/LI&gt;
&lt;LI&gt;an MCP resource server;&lt;/LI&gt;
&lt;LI&gt;an MCP client using the official Python SDK's identity-assertion APIs.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;The SDK's &lt;CODE&gt;IdentityAssertionOAuthProvider&lt;/CODE&gt; covers the second leg: the RFC 7523 JWT bearer grant from the MCP client to the MCP authorization server. The first leg, the RFC 8693 exchange with the enterprise IdP that produces the ID-JAG, is deployment-specific and supplied through the SDK's &lt;CODE&gt;assertion_provider&lt;/CODE&gt; callback. The lab fulfills that callback with its in-process stand-in IdP.&lt;/P&gt;
&lt;P&gt;The lab validates the ID-JAG signature, &lt;CODE&gt;typ&lt;/CODE&gt;, issuer, audience, client ID, resource, scopes, expiration, and single-use &lt;CODE&gt;jti&lt;/CODE&gt;. It also tests rejection cases for the wrong issuer, wrong audience, wrong resource, scope escalation, expiration, and replay. The lab deliberately requires a &lt;CODE&gt;resource&lt;/CODE&gt; claim, which is stricter than the stable specification's optional claim; production authorization servers should choose that posture explicitly.&lt;/P&gt;
&lt;P&gt;The stand-in IdP and in-memory token store are deliberately not production components. They make the protocol visible and testable without pretending Entra currently exposes the exact issuance flow used by the lab.&lt;/P&gt;
&lt;H2&gt;Run the EMA flow locally&lt;/H2&gt;
&lt;P&gt;Clone the repository and install the project:&lt;/P&gt;
&lt;PRE class="language-bash" tabindex="0" contenteditable="false" data-lia-code-value="git clone https://github.com/seligj95/app-service-ema-mcp
cd app-service-ema-mcp
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -e &amp;quot;.[dev]&amp;quot;
"&gt;&lt;CODE&gt;git clone https://github.com/seligj95/app-service-ema-mcp
cd app-service-ema-mcp
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -e ".[dev]"
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;Then run the complete ID-JAG exchange:&lt;/P&gt;
&lt;PRE class="language-bash" tabindex="0" contenteditable="false" data-lia-code-value="python -m examples.ema_lab.client
"&gt;&lt;CODE&gt;python -m examples.ema_lab.client
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;The final result is the identity and authorization context returned by the MCP server:&lt;/P&gt;
&lt;PRE class="language-text" tabindex="0" contenteditable="false" data-lia-code-value="{'subject': 'alice@example.com', 'client_id': 'finance-agent',
 'scopes': ['mcp:whoami'], 'resource': 'http://localhost/mcp'}
"&gt;&lt;CODE&gt;{'subject': 'alice@example.com', 'client_id': 'finance-agent',
 'scopes': ['mcp:whoami'], 'resource': 'http://localhost/mcp'}
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;Run the test suite to see both the successful exchange and the rejection boundaries:&lt;/P&gt;
&lt;PRE class="language-bash" tabindex="0" contenteditable="false" data-lia-code-value="python -m ruff check .
python -m pytest
"&gt;&lt;CODE&gt;python -m ruff check .
python -m pytest
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;One implementation note: at the time of writing, PyPI &lt;CODE&gt;mcp&lt;/CODE&gt; 1.28.1 package does not yet contain the identity-assertion APIs used by the stable extension. The sample pins an exact commit from the official Python SDK until those APIs ship in a release.&lt;/P&gt;
&lt;H2&gt;Deploy the Entra-governed MCP server&lt;/H2&gt;
&lt;P&gt;The repository includes azd and Bicep for a Basic B1 Linux App Service plan, Python 3.14, App Service Authentication, protected resource metadata, Log Analytics, Application Insights, and managed-identity telemetry.&lt;/P&gt;
&lt;P&gt;After choosing a subscription:&lt;/P&gt;
&lt;PRE class="language-bash" tabindex="0" contenteditable="false" data-lia-code-value="azd auth login
azd env new ema-mcp
azd env set AZURE_SUBSCRIPTION_ID &amp;lt;subscription-id&amp;gt;
azd env set AZURE_LOCATION eastus
./scripts/configure-entra-app.sh
azd up
"&gt;&lt;CODE&gt;azd auth login
azd env new ema-mcp
azd env set AZURE_SUBSCRIPTION_ID &amp;lt;subscription-id&amp;gt;
azd env set AZURE_LOCATION eastus
./scripts/configure-entra-app.sh
azd up
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;The Entra bootstrap runs once before &lt;CODE&gt;azd up&lt;/CODE&gt; because azd resolves required Bicep parameters before the preprovision hook. After that, the hook reruns the same idempotent configuration on each provision.&lt;/P&gt;
&lt;P&gt;Verify the deployed boundary:&lt;/P&gt;
&lt;PRE class="language-bash" tabindex="0" contenteditable="false" data-lia-code-value="./scripts/verify-deployment.sh
"&gt;&lt;CODE&gt;./scripts/verify-deployment.sh
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;The script checks that protected resource metadata is available, an anonymous MCP request is rejected, Azure CLI can obtain the delegated API token, and the same MCP request succeeds with that token.&lt;/P&gt;
&lt;P&gt;During deployment validation, I invoked &lt;CODE&gt;whoami&lt;/CODE&gt; against the deployed app. App Service returned the expected Azure CLI client and the delegated scope:&lt;/P&gt;
&lt;PRE class="language-json" tabindex="0" contenteditable="false" data-lia-code-value="{
  &amp;quot;authentication_type&amp;quot;: &amp;quot;aad&amp;quot;,
  &amp;quot;client_id&amp;quot;: &amp;quot;04b07795-8ddb-461a-bbee-02f9e1bf7b46&amp;quot;,
  &amp;quot;scopes&amp;quot;: [&amp;quot;user_impersonation&amp;quot;]
}
"&gt;&lt;CODE&gt;{
  "authentication_type": "aad",
  "client_id": "04b07795-8ddb-461a-bbee-02f9e1bf7b46",
  "scopes": ["user_impersonation"]
}
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;The corresponding custom trace arrived in Application Insights through the App Service managed identity, with local telemetry authentication disabled.&lt;/P&gt;
&lt;P&gt;The response above is shortened for readability. The complete tool result also includes &lt;CODE&gt;name&lt;/CODE&gt;, &lt;CODE&gt;subject&lt;/CODE&gt;, &lt;CODE&gt;tenant_id&lt;/CODE&gt;, and &lt;CODE&gt;roles&lt;/CODE&gt;.&lt;/P&gt;
&lt;H2&gt;Three details worth carrying into production&lt;/H2&gt;
&lt;P&gt;The platform did the hard security work, but three integration details were easy to miss.&lt;/P&gt;
&lt;H3&gt;1. Allow the audience Entra actually emits&lt;/H3&gt;
&lt;P&gt;The client requests &lt;CODE&gt;api://&amp;lt;application-id&amp;gt;/user_impersonation&lt;/CODE&gt;, but a v2 delegated access token can carry the bare application client ID in &lt;CODE&gt;aud&lt;/CODE&gt;. The sample allows both the bare client ID and the &lt;CODE&gt;api://&lt;/CODE&gt; form while still rejecting tokens for every other resource.&lt;/P&gt;
&lt;H3&gt;2. Keep the SDK's DNS-rebinding protection&lt;/H3&gt;
&lt;P&gt;The MCP Python SDK defaults to localhost when a server does not declare its deployment hostname. That is a safe local default, but a deployed request correctly fails with &lt;CODE&gt;421 Invalid Host header&lt;/CODE&gt;.&lt;/P&gt;
&lt;P&gt;The sample reads &lt;CODE&gt;WEBSITE_HOSTNAME&lt;/CODE&gt; and allowlists only that exact App Service hostname and its optional port form. It does not disable the protection.&lt;/P&gt;
&lt;H3&gt;3. Account for App Service claim mapping&lt;/H3&gt;
&lt;P&gt;App Service maps some JWT claim names before it builds &lt;CODE&gt;X-MS-CLIENT-PRINCIPAL&lt;/CODE&gt;. For example, the delegated &lt;CODE&gt;scp&lt;/CODE&gt; value can arrive under the mapped scope claim URI.&lt;/P&gt;
&lt;P&gt;The application accepts both representations, while continuing to trust only the principal header injected by App Service. A caller-supplied identity header never becomes proof of authentication because the platform strips and replaces these headers at the edge.&lt;/P&gt;
&lt;H2&gt;The production checklist&lt;/H2&gt;
&lt;P&gt;This sample gives you the authorization boundary, not every production control. Before using the pattern for a real tool surface:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Apply Conditional Access deliberately, starting in report-only mode.&lt;/LI&gt;
&lt;LI&gt;Add tool-level scope or role checks when different tools have different sensitivity.&lt;/LI&gt;
&lt;LI&gt;Use managed identity or an explicit on-behalf-of flow for downstream APIs; never forward the MCP resource token.&lt;/LI&gt;
&lt;LI&gt;Add private networking or an API gateway when the workload requires network isolation.&lt;/LI&gt;
&lt;LI&gt;Keep access tokens and ID-JAGs short-lived.&lt;/LI&gt;
&lt;LI&gt;Validate ID-JAG signatures with the enterprise IdP's published JWKS, discovered from its authorization server metadata. The lab's shared HMAC key exists only to keep the local flow self-contained.&lt;/LI&gt;
&lt;LI&gt;Enforce single-use ID-JAG replay protection at the authorization server.&lt;/LI&gt;
&lt;LI&gt;Log authorization decisions without logging bearer tokens or assertion contents.&lt;/LI&gt;
&lt;LI&gt;Replace the SDK Git pin after the identity-assertion APIs reach a tested package release.&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2&gt;The takeaway&lt;/H2&gt;
&lt;P&gt;EMA is bigger than eliminating another consent screen. It defines how an enterprise IdP becomes the authorization control plane for an MCP fleet, with a portable assertion that an MCP authorization server can validate.&lt;/P&gt;
&lt;P&gt;Entra ID and App Service already give you many of the outcomes enterprises need: tenant policy, known-client governance, protected resource discovery, platform token validation, Conditional Access, and centralized telemetry.&lt;/P&gt;
&lt;P&gt;Just name the boundary honestly.&lt;/P&gt;
&lt;P&gt;Use App Service and Easy Auth when you want a deployable, Entra-governed MCP endpoint today. If your IdP already supports ID-JAG issuance, App Service can also host a custom EMA authorization implementation. Otherwise, use the local lab to understand and test the protocol while platform-native support develops.&lt;/P&gt;
&lt;P&gt;That is the part I like most about the extension: enterprise authorization stops being an experience every MCP server invents and becomes a protocol the ecosystem can share.&lt;/P&gt;
&lt;H2&gt;Resources&lt;/H2&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;A class="lia-external-url" href="https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization" target="_blank" rel="noopener"&gt;Enterprise-Managed Authorization extension&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A class="lia-external-url" href="https://blog.modelcontextprotocol.io/posts/enterprise-managed-auth/" target="_blank" rel="noopener"&gt;Enterprise-Managed Authorization announcement&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A class="lia-external-url" href="https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/stable/enterprise-managed-authorization.mdx" target="_blank" rel="noopener"&gt;Stable EMA specification&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A class="lia-external-url" href="https://github.com/modelcontextprotocol/python-sdk/blob/main/docs/client/identity-assertion.md" target="_blank" rel="noopener"&gt;MCP Python SDK identity assertion guide&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/app-service/configure-authentication-mcp" target="_blank" rel="noopener"&gt;Configure MCP server authorization on App Service&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/app-service/configure-authentication-mcp-server-vscode" target="_blank" rel="noopener"&gt;Secure MCP calls from Visual Studio Code&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A class="lia-external-url" href="https://github.com/seligj95/app-service-ema-mcp" target="_blank" rel="noopener"&gt;Companion sample&lt;/A&gt;&lt;/LI&gt;
&lt;/UL&gt;</description>
      <pubDate>Thu, 16 Jul 2026 14:31:39 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/mcp-enterprise-authorization-is-here-what-entra-and-app-service/ba-p/4537433</guid>
      <dc:creator>jordanselig</dc:creator>
      <dc:date>2026-07-16T14:31:39Z</dc:date>
    </item>
    <item>
      <title>How Microsoft 365 built a platform engineering layer on AKS to ship faster at global scale</title>
      <link>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/how-microsoft-365-built-a-platform-engineering-layer-on-aks-to/ba-p/4537523</link>
      <description>&lt;H4 aria-level="1"&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt;Challenge&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt;: Scaling success&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt;without&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt;increasing&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt;&amp;nbsp;complexity&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt; &lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:360,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/H4&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Microsoft 365 runs a broad portfolio of services across global and sovereign clouds. At that scale, the platform&amp;nbsp;has to&amp;nbsp;do more than schedule workloads. It&amp;nbsp;has to&amp;nbsp;enforce security boundaries, meet enterprise compliance requirements, and support reliable change management across every service that runs on it.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Without a shared foundation, each service team ended up solving the same problems on its own: how to provision correctly, deploy safely, meet policy requirements, and stay resilient as traffic and complexity grew. That slowed teams down and produced unnecessary variation in how services were built and&amp;nbsp;operated.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;What Microsoft 365 needed was a hosting experience that could absorb operational complexity&amp;nbsp;centrally&amp;nbsp;so product teams could focus on customer value. &lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;This is why building on&amp;nbsp;Kubernetes was a natural choice for us.&amp;nbsp;Kubernetes gives Microsoft&amp;nbsp;365 the ability&amp;nbsp;to&amp;nbsp;run and&amp;nbsp;grow&amp;nbsp;at&amp;nbsp;the scale the service demands&amp;nbsp;without sacrificing consistency.&amp;nbsp;But we also knew that&amp;nbsp;growth&amp;nbsp;can&amp;nbsp;increase&amp;nbsp;system complexity&amp;nbsp;without the right management in place.&amp;nbsp;As&amp;nbsp;services&amp;nbsp;and&amp;nbsp;team sizes&amp;nbsp;increase,&amp;nbsp;we&amp;nbsp;needed&amp;nbsp;systems&amp;nbsp;that&amp;nbsp;mitigate&amp;nbsp;these issues.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; &lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Infrastructure and operational complexity grows faster than teams unless the platform absorbs that complexity. Service teams historically solved infrastructure challenges independently.&amp;nbsp;Each team&amp;nbsp;was able&amp;nbsp;to create their&amp;nbsp;own solution with&amp;nbsp;Azure,&amp;nbsp;but&amp;nbsp;without a&amp;nbsp;standardized platform, we risked&amp;nbsp;trending&amp;nbsp;towards&amp;nbsp;unnecessary operational overhead. As services scaled,&amp;nbsp;we&amp;nbsp;needed&amp;nbsp;to&amp;nbsp;mitigate&amp;nbsp;the reality of: &lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="21" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;multilevel&amp;quot;}" data-aria-posinset="1" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Inconsistency&amp;nbsp;and configuration drift&amp;nbsp;across environments &lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="21" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;multilevel&amp;quot;}" data-aria-posinset="2" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Increased&amp;nbsp;infrastructure&amp;nbsp;work&amp;nbsp;pulling developers away from product work&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="21" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;multilevel&amp;quot;}" data-aria-posinset="3" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Increased risk&amp;nbsp;and uncertainty&amp;nbsp;in&amp;nbsp;deployments&amp;nbsp;and change&amp;nbsp;management &lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;We needed a platform layer that could absorb this complexity before it became a tax on every team.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H4 aria-level="1"&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt;Solution&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt;: Evolving the operations experience&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt; &lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:360,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/H4&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Microsoft 365 uses AKS as the foundation for a standardized hosting layer. AKS provides the orchestration, Azure integration, and operational primitives needed for large-scale environments. Teams&amp;nbsp;builds&amp;nbsp;on that foundation with Microsoft 365-specific patterns, guardrails, and workflows.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;That hosting layer gives teams a more consistent path from onboarding to production. Provisioning is aligned to predefined architectures and regional boundaries. Security and compliance controls are applied centrally instead of left to every service team to implement independently. Standardized deployment practices reduce change risk, while shared telemetry improves visibility into health and policy adherence.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;The result is a model where developers hand over service intent and application code, while the platform supplies the hosted environment, deployment safety,&amp;nbsp;security and compliance guardrails,&amp;nbsp;and operational scaffolding needed to run well at scale.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H6 aria-level="2"&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;COSMIC: Microsoft 365’s Platform&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;Hosting&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;Layer &lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/H6&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;This is exactly why we created&amp;nbsp;COSMIC&amp;nbsp;which&amp;nbsp;is&amp;nbsp;Microsoft 365’s&amp;nbsp;platform&amp;nbsp;engineering&amp;nbsp;layer&amp;nbsp;for standardizing hyper-scale cloud services&amp;nbsp;on AKS.&amp;nbsp;Our goal&amp;nbsp;with COSMIC&amp;nbsp;and its platform engineering team&amp;nbsp;was simple: “give us the code and allow us to do the rest.”&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;COSMIC was designed to create a secure, compliant, common tech stack that removes infrastructure burden from application teams, allowing them to focus on innovative feature work while COSMIC provides a full-service, standardized platform. It functions as an interface layer on top of AKS and, with the help of the COSMIC team, tailors the experience to Microsoft 365’s needs: &lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="11" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="1" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;A secure and compliant default posture&lt;/STRONG&gt;.&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;&amp;nbsp;Services inherit standardized controls for identity, access, certificates and secrets, approved base images, and policy enforcement.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="11" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="1" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;A streamlined provisioning and onboarding path&lt;/STRONG&gt;.&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; Teams are mapped to predefined deployment shapes, regional placement patterns, and hosted environments that reduce setup friction and improve consistency.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="11" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="1" data-aria-level="1"&gt;&lt;STRONG&gt;Safer deployments and stronger resiliency.&lt;/STRONG&gt;&lt;SPAN style="color: rgb(30, 30, 30);" data-contrast="auto"&gt;&amp;nbsp;Standard rollout patterns, stage validation, telemetry-driven checks, and controlled change boundaries help teams release with lower risk and recover more predictably.&lt;/SPAN&gt;&lt;SPAN style="color: rgb(30, 30, 30);" data-ccp-props="{&amp;quot;335559685&amp;quot;:720}"&gt; &lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="11" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="2" data-aria-level="1"&gt;&lt;STRONG&gt;Operational efficiency at scale.&lt;/STRONG&gt;&lt;SPAN style="color: rgb(30, 30, 30);" data-contrast="auto"&gt;&amp;nbsp;Shared observability, common dashboards, and platform-managed runtime practices reduce duplicated effort while helping teams focus on feature delivery instead of infrastructure mechanics.&lt;/SPAN&gt;&lt;SPAN style="color: rgb(30, 30, 30);" data-ccp-props="{&amp;quot;335559685&amp;quot;:720}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;At a high level, product teams submit a request to COSMIC for the infrastructure and services needed to run their workloads. Based on a service’s requirements, COSMIC provisions infrastructure using standardized architectures with built-in security, compliance, and regional controls. The platform also helps place workloads efficiently and manage capacity at scale.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Once infrastructure is provisioned, teams adopt standardized deployment and operational workflows that automatically inherit Microsoft 365 security, compliance, and reliability standards.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;This includes:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="9" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;multilevel&amp;quot;}" data-aria-posinset="1" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;Identity, authorization, and secrets management:&lt;/STRONG&gt;&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; standardized authentication and authorization patterns, automated certificate management, and centralized secrets management using Azure Key Vault to provide consistent credential handling across the service lifecycle&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="9" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;multilevel&amp;quot;}" data-aria-posinset="1" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;Standardized and secure runtime environments:&lt;/STRONG&gt;&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;&amp;nbsp;standardized base container images with enforced patching and security updates to keep workloads current with the latest security and platform improvements&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="9" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;multilevel&amp;quot;}" data-aria-posinset="1" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;Built-in resiliency and scalable operations:&lt;/STRONG&gt;&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;&amp;nbsp;automated scaling, regional placement controls, health-based recovery, and deployment safety mechanisms to help services remain reliable during traffic spikes, failures, and growth&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="9" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;multilevel&amp;quot;}" data-aria-posinset="1" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;Rich observability and telemetry:&lt;/STRONG&gt;&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; standardized logging, metrics, and monitoring that help teams detect, diagnose, and mitigate issues across both the COSMIC platform and the applications running on it.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="9" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;multilevel&amp;quot;}" data-aria-posinset="1" data-aria-level="1"&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Managed deployments and infrastructure automation:&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&amp;nbsp;centralized deployment systems with built-in validation, compliance enforcement, safe rollout practices, and automated recovery. Infrastructure and environment configuration are standardized through reusable templates and automated workflows, reducing operational overhead as services scale.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;H4 aria-level="1"&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt;Impact&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt;:&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt;A standardized platform that delights developers&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:360,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/H4&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;For Microsoft 365 service teams, COSMIC makes the hosted experience more predictable. Teams do not have to become experts in every layer of platform operations&amp;nbsp;in order to&amp;nbsp;build and run&amp;nbsp;business-critical&amp;nbsp;services. Instead, we unlock:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="23" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;multilevel&amp;quot;}" data-aria-posinset="1" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Faster, more consistent&amp;nbsp;deployments&amp;nbsp;–&amp;nbsp;shipping to production is simpler and more predictable, reducing decision fatigue&amp;nbsp;for developers. &lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="23" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;multilevel&amp;quot;}" data-aria-posinset="2" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Team autonomy preserved -&amp;nbsp;service teams keep their choice in availability using a self-managed environment&amp;nbsp;within COSMIC’s guardrails.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="14" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;multilevel&amp;quot;}" data-aria-posinset="1" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Security and compliance&amp;nbsp;by default – trust requirements&amp;nbsp;are “baked in” to the experience and&amp;nbsp;enforced&amp;nbsp;through the platform, not through manual checklists.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;COSMIC&amp;nbsp;is now one of the most&amp;nbsp;widely-adopted&amp;nbsp;internal platforms at&amp;nbsp;Microsoft,&amp;nbsp;with a&amp;nbsp;continually&amp;nbsp;positive&amp;nbsp;growth&amp;nbsp;trend. It’s&amp;nbsp;one of our&amp;nbsp;clearest&amp;nbsp;win-win&amp;nbsp;outcomes: developers&amp;nbsp;focus on&amp;nbsp;the&amp;nbsp;work they love&amp;nbsp;while&amp;nbsp;the&amp;nbsp;platform team&amp;nbsp;maintains&amp;nbsp;systems&amp;nbsp;as&amp;nbsp;the experts&amp;nbsp;in&amp;nbsp;operational excellence.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H4 aria-level="1"&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt;Learnings&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt;: Building your own platform&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt; &lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:360,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/H4&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Operating Kubernetes at enterprise scale requires&amp;nbsp;simplifying trust, compliance, and reliability&amp;nbsp;into the platform. Additionally,&amp;nbsp;developer experience is key: developer&amp;nbsp;and product team&amp;nbsp;autonomy scales best when guardrails are strong and&amp;nbsp;experience is consistent. &lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;These properties don’t emerge organically;&amp;nbsp;they require&amp;nbsp;a modern&amp;nbsp;operating model driven by&amp;nbsp;a team of platform experts.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;When creating your organization’s model, we&amp;nbsp;recommend keeping&amp;nbsp;some of our learnings in mind:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="13" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="1" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;Limit blast radius in change management -&lt;/STRONG&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;safe deployment practices for all changes are critical.&amp;nbsp;Changes are contained within defined boundaries, with the ability to reroute traffic or fail over to backup services to minimize risk. Every deployment follows the same standardized validation and rollout process.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="13" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="2" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;Create a turnkey experience for service teams -&lt;/STRONG&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;product teams should focus on building great user experiences, not becoming infrastructure experts. COSMIC&amp;nbsp;abstracts away&amp;nbsp;operational complexity such as cluster configuration, patching, scaling policies, pod disruption management, and image lifecycle management so teams can move faster with less operational overhead.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="13" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="3" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;Resiliency and best practices baked in -&lt;/STRONG&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;services inherit platform-level resiliency patterns by default, including automated scaling, health-based recovery, deployment safety checks, regional distribution, and standardized operational guardrails.&amp;nbsp;This helps teams build reliable services without having to reimplement foundational operational practices themselves.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="5" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="1" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;Aggregate observability around what matters -&lt;/STRONG&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;teams get pre-built dashboards and standardized telemetry experiences, while role-based access controls ensure the right people see the right data. Service teams can focus on their&amp;nbsp;application&amp;nbsp;health, while platform operators&amp;nbsp;monitor&amp;nbsp;fleet-wide trends across reliability, performance,&amp;nbsp;utilization, and cost efficiency. This helps reduce noise and surface the signals that matter most.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;H6&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;Turning learnings into features&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt; &lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/H6&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Microsoft&amp;nbsp;has&amp;nbsp;absorbed the hardest parts of operating Kubernetes at scale&amp;nbsp;and encoded lessons from&amp;nbsp;COSMIC&amp;nbsp;into Azure Kubernetes Service. Through those&amp;nbsp;learnings, we&amp;nbsp;build features that minimize pain points&amp;nbsp;for our customers. Our experience with COSMIC&amp;nbsp;informed&amp;nbsp;new&amp;nbsp;features that&amp;nbsp;make building your&amp;nbsp;own&amp;nbsp;platform faster and easier:&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="4" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;multilevel&amp;quot;}" data-aria-posinset="1" data-aria-level="1"&gt;&lt;A href="https://azure.microsoft.com/en-us/blog/azure-kubernetes-service-automatic-fast-and-frictionless-kubernetes-for-all/" target="_blank" rel="noopener"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-charstyle="Hyperlink"&gt;AKS Automatic&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN data-contrast="auto"&gt;:&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;&amp;nbsp;this is how we bring the simplified experience developers needed to customers. AKS Automatic&amp;nbsp;lets&amp;nbsp;developers&amp;nbsp;to create production-ready Kubernetes clusters with built-in best practices and guardrails while Azure handles the node management, scaling, and security.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="17" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;multilevel&amp;quot;}" data-aria-posinset="1" data-aria-level="1"&gt;&lt;A href="https://learn.microsoft.com/en-us/azure/kubernetes-fleet/overview" target="_blank" rel="noopener"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-charstyle="Hyperlink"&gt;Azure Kubernetes Fleet Manager&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN data-contrast="auto"&gt;:&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; Operating&amp;nbsp;a&amp;nbsp;large, globally distributed fleet&amp;nbsp;requires&amp;nbsp;predictable,&amp;nbsp;low risk&amp;nbsp;ways to&amp;nbsp;coordinate safe multi-cluster upgrades,&amp;nbsp;load balance traffic across clusters,&amp;nbsp;and centralize control across regions and clouds.&amp;nbsp;The same capabilities&amp;nbsp;that enable fleet-level orchestration&amp;nbsp;for&amp;nbsp;ourselves&amp;nbsp;are available for our customers.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="6" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;multilevel&amp;quot;}" data-aria-posinset="1" data-aria-level="1"&gt;&lt;A href="https://learn.microsoft.com/en-us/azure/application-network/observability" target="_blank" rel="noopener"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-charstyle="Hyperlink"&gt;Expanded observability&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN data-contrast="auto"&gt;:&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; Managed Prometheus and Grafana are integrated with AKS and Azure Monitor for&amp;nbsp;secure, compliant, and cost-effective monitoring,&amp;nbsp;enabling&amp;nbsp;actionable insights at scale&amp;nbsp;without drowning in data or compromising on security and compliance.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;H4 aria-level="1"&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt;What’s next and CTAs&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:360,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/H4&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="6" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;multilevel&amp;quot;}" data-aria-posinset="2" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;See the&amp;nbsp;&lt;/SPAN&gt;&lt;A href="https://github.com/orgs/Azure/projects/685" target="_blank" rel="noopener"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-charstyle="Hyperlink"&gt;Azure Kubernetes Service roadmap&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="6" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;multilevel&amp;quot;}" data-aria-posinset="3" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Learn more about&amp;nbsp;&lt;/SPAN&gt;&lt;A href="https://azure.microsoft.com/en-us/products/kubernetes-service" target="_blank" rel="noopener"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-charstyle="Hyperlink"&gt;Azure Kubernetes Service&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="6" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;multilevel&amp;quot;}" data-aria-posinset="4" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Read more about&amp;nbsp;&lt;/SPAN&gt;&lt;A href="https://aka.ms/customerzeroblogs" target="_blank" rel="noopener"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-charstyle="Hyperlink"&gt;Microsoft as Customer Zero&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;</description>
      <pubDate>Thu, 16 Jul 2026 05:00:13 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/how-microsoft-365-built-a-platform-engineering-layer-on-aks-to/ba-p/4537523</guid>
      <dc:creator>Suma SaganeGowda</dc:creator>
      <dc:date>2026-07-16T05:00:13Z</dc:date>
    </item>
    <item>
      <title>Give your AI agent two memories with Azure App Service</title>
      <link>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/give-your-ai-agent-two-memories-with-azure-app-service/ba-p/4537400</link>
      <description>&lt;P&gt;Agents feel continuous only when they can operate across two very different time horizons. They need the recent turns that make the current conversation coherent, and they need a smaller set of durable facts that can follow an authenticated user into a new conversation.&lt;/P&gt;
&lt;P&gt;This sample implements both horizons on Azure App Service with Microsoft Agent Framework, Azure Managed Redis, Azure Cosmos DB for NoSQL vector search, and Azure OpenAI. It is deployed and available as a complete reference implementation with a browser UI, deterministic local mode, tests, Bicep, and Azure Developer CLI support.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Sample:&lt;/STRONG&gt; &lt;A class="lia-external-url" href="https://github.com/seligj95/app-service-agent-memory" target="_blank" rel="noopener"&gt;github.com/seligj95/app-service-agent-memory&lt;/A&gt;&lt;/P&gt;
&lt;H2&gt;Why one memory store is not enough&lt;/H2&gt;
&lt;P&gt;&lt;STRONG&gt;Conversation history&lt;/STRONG&gt; is ordered, session-specific, frequently updated, and naturally short-lived. The agent needs it to resolve statements such as “use the second option” or “what did I just say?”&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Durable memory&lt;/STRONG&gt; is selective, user-scoped, and useful across sessions. It holds facts such as a preferred deployment region, product name, accessibility need, or writing preference. Retrieval is semantic rather than chronological.&lt;/P&gt;
&lt;P&gt;Putting both into one unbounded prompt makes cost, latency, privacy, and deletion harder to reason about. The sample instead gives each horizon a purpose-built store and joins them through the Agent Framework context pipeline.&lt;/P&gt;
&lt;H2&gt;The Azure architecture&lt;/H2&gt;
&lt;P&gt;The public FastAPI application runs on one always-on App Service Premium v4 instance with Python 3.13. The browser creates demo user and conversation IDs in local storage. That keeps the sample easy to explore, but it is not production authentication.&lt;/P&gt;
&lt;P&gt;For every chat turn, the application:&lt;/P&gt;
&lt;OL&gt;
&lt;LI&gt;Validates the user ID, session ID, message, and retrieval limit.&lt;/LI&gt;
&lt;LI&gt;Loads bounded conversation history from Azure Managed Redis.&lt;/LI&gt;
&lt;LI&gt;Creates an embedding for the new input.&lt;/LI&gt;
&lt;LI&gt;Runs a partition-scoped vector query in Cosmos DB for the same user.&lt;/LI&gt;
&lt;LI&gt;Adds relevant durable memories to the Agent Framework context.&lt;/LI&gt;
&lt;LI&gt;Runs &lt;CODE&gt;gpt-5-mini&lt;/CODE&gt;.&lt;/LI&gt;
&lt;LI&gt;Stores the new conversation messages in Redis and refreshes their TTL.&lt;/LI&gt;
&lt;LI&gt;Extracts conservative durable facts, embeds them, deduplicates them, and upserts them to Cosmos DB.&lt;/LI&gt;
&lt;/OL&gt;
&lt;P&gt;The chat response also returns memory attribution so the UI can show that a memory influenced the turn.&lt;/P&gt;
&lt;img /&gt;
&lt;H2&gt;Short-term history with a custom HistoryProvider&lt;/H2&gt;
&lt;P&gt;Agent Framework's current Python API makes history a context provider. The custom provider only implements the storage boundary; the framework handles when to load and persist messages.&lt;/P&gt;
&lt;PRE class="language-python" tabindex="0" contenteditable="false" data-lia-code-value="class RedisHistoryProvider(HistoryProvider):
    def __init__(self, store, ttl_seconds, max_messages=40):
        super().__init__(&amp;quot;redis-history&amp;quot;)
        self._store = store
        self._ttl_seconds = ttl_seconds
        self._max_messages = max_messages

    async def get_messages(self, session_id, *, state=None, **kwargs):
        user_id = _required_state_value(state, &amp;quot;user_id&amp;quot;)
        values = await self._store.load(user_id, session_id)
        return [Message.from_dict(json.loads(value))
                for value in values[-self._max_messages:]]

    async def save_messages(self, session_id, messages, *, state=None, **kwargs):
        user_id = _required_state_value(state, &amp;quot;user_id&amp;quot;)
        values = [json.dumps(message.to_dict()) for message in messages]
        await self._store.append(
            user_id, session_id, values, self._ttl_seconds, self._max_messages
        )"&gt;&lt;CODE&gt;class RedisHistoryProvider(HistoryProvider):
    def __init__(self, store, ttl_seconds, max_messages=40):
        super().__init__("redis-history")
        self._store = store
        self._ttl_seconds = ttl_seconds
        self._max_messages = max_messages

    async def get_messages(self, session_id, *, state=None, **kwargs):
        user_id = _required_state_value(state, "user_id")
        values = await self._store.load(user_id, session_id)
        return [Message.from_dict(json.loads(value))
                for value in values[-self._max_messages:]]

    async def save_messages(self, session_id, messages, *, state=None, **kwargs):
        user_id = _required_state_value(state, "user_id")
        values = [json.dumps(message.to_dict()) for message in messages]
        await self._store.append(
            user_id, session_id, values, self._ttl_seconds, self._max_messages
        )&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;The Redis key is &lt;CODE&gt;session:{user_id}:{session_id}&lt;/CODE&gt;. Each append also trims the list and refreshes the seven-day TTL. Serializing the complete Agent Framework &lt;CODE&gt;Message&lt;/CODE&gt; preserves tool and attribution metadata instead of reducing history to plain strings.&lt;/P&gt;
&lt;H2&gt;Durable recall with a custom ContextProvider&lt;/H2&gt;
&lt;P&gt;The durable provider participates before and after the model call.&lt;/P&gt;
&lt;PRE class="language-python" tabindex="0" contenteditable="false" data-lia-code-value="class CosmosContextProvider(ContextProvider):
    async def before_run(self, *, agent, session, context, state):
        user_id = _required_state_value(state, &amp;quot;user_id&amp;quot;)
        embedding = await self._embeddings.embed(_latest_input_text(context))
        recalled = await self._store.recall(
            user_id, embedding, self._recall_limit
        )
        state[&amp;quot;recalled_memories&amp;quot;] = [
            item.model_dump(mode=&amp;quot;json&amp;quot;) for item in recalled
        ]

        if recalled:
            facts = &amp;quot;\n&amp;quot;.join(f&amp;quot;- {item.text}&amp;quot; for item in recalled)
            context.extend_instructions(
                self.source_id,
                &amp;quot;Use these durable memories only when relevant:\n&amp;quot; + facts,
            )

    async def after_run(self, *, agent, session, context, state):
        for fact, category in extract_durable_facts(_latest_input_text(context)):
            embedding = await self._embeddings.embed(fact)
            await self._store.remember(
                state[&amp;quot;user_id&amp;quot;],
                fact,
                category,
                state[&amp;quot;source_turn&amp;quot;],
                embedding,
            )"&gt;&lt;CODE&gt;class CosmosContextProvider(ContextProvider):
    async def before_run(self, *, agent, session, context, state):
        user_id = _required_state_value(state, "user_id")
        embedding = await self._embeddings.embed(_latest_input_text(context))
        recalled = await self._store.recall(
            user_id, embedding, self._recall_limit
        )
        state["recalled_memories"] = [
            item.model_dump(mode="json") for item in recalled
        ]

        if recalled:
            facts = "\n".join(f"- {item.text}" for item in recalled)
            context.extend_instructions(
                self.source_id,
                "Use these durable memories only when relevant:\n" + facts,
            )

    async def after_run(self, *, agent, session, context, state):
        for fact, category in extract_durable_facts(_latest_input_text(context)):
            embedding = await self._embeddings.embed(fact)
            await self._store.remember(
                state["user_id"],
                fact,
                category,
                state["source_turn"],
                embedding,
            )&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;The Cosmos container uses &lt;CODE&gt;/user_id&lt;/CODE&gt; as its partition key and a 1,536-dimension cosine &lt;CODE&gt;quantizedFlat&lt;/CODE&gt; vector index. Recall is always routed to one user's partition and is bounded to a small result set. A stable ID derived from the user scope and normalized content hash makes writes idempotent.&lt;/P&gt;
&lt;H2&gt;Passwordless by default&lt;/H2&gt;
&lt;P&gt;App Service uses its system-assigned managed identity for every data service.&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Azure OpenAI:&lt;/STRONG&gt; Cognitive Services OpenAI User&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Cosmos DB:&lt;/STRONG&gt; native built-in data contributor&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Key Vault:&lt;/STRONG&gt; Key Vault Secrets User&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Azure Managed Redis:&lt;/STRONG&gt; database-scoped Entra access-policy assignment&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Azure OpenAI and Cosmos DB local authentication are disabled. Redis requires TLS and Entra authentication, and its access keys are disabled. The application explicitly selects &lt;CODE&gt;ManagedIdentityCredential&lt;/CODE&gt; in Azure and &lt;CODE&gt;AzureCliCredential&lt;/CODE&gt; for local real-service development. It does not use a broad production credential chain.&lt;/P&gt;
&lt;P&gt;Key Vault remains the secrets boundary for future extensions, although this passwordless sample does not need a runtime secret.&lt;/P&gt;
&lt;H2&gt;Try it locally without Azure&lt;/H2&gt;
&lt;P&gt;The deterministic fake mode exercises the real provider pipeline and complete UI without an Azure subscription:&lt;/P&gt;
&lt;PRE class="language-bash" tabindex="0" contenteditable="false" data-lia-code-value="uv sync --python 3.13 --all-groups
uv run uvicorn app.main:app --reload"&gt;&lt;CODE&gt;uv sync --python 3.13 --all-groups
uv run uvicorn app.main:app --reload&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;Open &lt;CODE&gt;http://127.0.0.1:8000&lt;/CODE&gt;, tell the agent “My favorite launch color is teal,” start a new conversation, and ask for the color. You can inspect attribution, list the stored memory, and forget it.&lt;/P&gt;
&lt;H2&gt;Deploy with Azure Developer CLI&lt;/H2&gt;
&lt;P&gt;After creating an azd environment and setting its subscription and supported region, deployment is one command:&lt;/P&gt;
&lt;PRE class="language-bash" tabindex="0" contenteditable="false" data-lia-code-value="azd up --no-prompt"&gt;&lt;CODE&gt;azd up --no-prompt&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;The Bicep creates App Service, Managed Redis, Cosmos DB, Azure OpenAI model deployments, Key Vault, Application Insights, and Log Analytics. A smoke test then checks health, same-session history, explicit remember, new-session recall, list, forget, and absence after forget.&lt;/P&gt;
&lt;P&gt;Azure Managed Redis availability is subscription- and region-dependent. The deployed reference uses East US 2 after the service preflight rejected East US for this subscription.&lt;/P&gt;
&lt;H2&gt;What the demo deliberately does not hide&lt;/H2&gt;
&lt;P&gt;The browser identity is anonymous and user-controlled. That is useful for understanding the data flow, but a production application must replace it with authenticated claims and authorization on every memory operation.&lt;/P&gt;
&lt;P&gt;The sample also keeps one App Service instance. Before scaling out, replace the in-process conversation lock with a distributed lock so concurrent requests cannot reorder one session's history.&lt;/P&gt;
&lt;P&gt;Other production work includes private endpoints and VNet integration, consent and retention policy, user export and deletion, content safety, prompt-injection defenses, abuse throttling, per-user quotas, and evaluation of retrieval thresholds.&lt;/P&gt;
&lt;H2&gt;Learn more&lt;/H2&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;A class="lia-external-url" href="https://learn.microsoft.com/agent-framework/get-started/memory" target="_blank" rel="noopener"&gt;Microsoft Agent Framework memory&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A class="lia-external-url" href="https://learn.microsoft.com/agent-framework/agents/conversations/context-providers" target="_blank" rel="noopener"&gt;Agent Framework context providers&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/app-service/configure-language-python" target="_blank" rel="noopener"&gt;Configure Python on Azure App Service&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/redis/entra-for-authentication" target="_blank" rel="noopener"&gt;Use Microsoft Entra ID with Azure Managed Redis&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/cosmos-db/how-to-python-vector-index-query" target="_blank" rel="noopener"&gt;Vector search in Azure Cosmos DB for NoSQL&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/developer/ai/keyless-connections" target="_blank" rel="noopener"&gt;Use Azure OpenAI without keys&lt;/A&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Two memory horizons make the agent easier to operate and easier to trust: session history remains temporary, durable memory remains selective and user-scoped, and both have explicit lifecycle controls.&lt;/P&gt;</description>
      <pubDate>Wed, 15 Jul 2026 17:23:43 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/give-your-ai-agent-two-memories-with-azure-app-service/ba-p/4537400</guid>
      <dc:creator>jordanselig</dc:creator>
      <dc:date>2026-07-15T17:23:43Z</dc:date>
    </item>
    <item>
      <title>Load testing Copilot Studio agents with Locust and Azure Load Testing</title>
      <link>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/load-testing-copilot-studio-agents-with-locust-and-azure-load/ba-p/4535445</link>
      <description>&lt;P&gt;A conversational agent doesn't answer like an API. You send one message. The server returns&amp;nbsp;200 OK. And then nothing happens. The real answer is still coming, streaming back over a WebSocket as one reply or several. It might take two seconds, or three minutes while the agent thinks, calls a tool, or even builds another agent. So how do you load test an answer that isn't ready when the request says it is?&lt;/P&gt;
&lt;P&gt;This post builds that test — and one 30-minute run answered it:&amp;nbsp;&lt;STRONG&gt;253 conversations and 3,212 requests with zero failures&lt;/STRONG&gt;, including an agent-creation turn that took&amp;nbsp;&lt;STRONG&gt;8.68 seconds to reply and 163.71 seconds to finish&lt;/STRONG&gt;.&lt;/P&gt;
&lt;H2&gt;Why load test a Copilot Studio agent&lt;/H2&gt;
&lt;P&gt;Conversational agents built with Copilot Studio run on a platform that automatically scales to support increases in demand and load, as documented in Microsoft's&amp;nbsp;&lt;A href="https://learn.microsoft.com/en-us/microsoft-copilot-studio/guidance/conversational-agents-performance-testing" target="_blank" rel="noopener"&gt;performance-testing guidance&lt;/A&gt;.&amp;nbsp;&lt;A href="https://learn.microsoft.com/en-us/microsoft-copilot-studio/guidance/plan-agent-throughput-rate-limits#rate-provisioning-is-separate-from-license-provisioning" target="_blank" rel="noopener"&gt;That scaling is not infinite&lt;/A&gt;. It stays within the environment's capacity,&amp;nbsp;&lt;A href="https://learn.microsoft.com/en-us/microsoft-copilot-studio/requirements-quotas" target="_blank" rel="noopener"&gt;quotas&lt;/A&gt;, throttling, and service limits. A turn can also reach custom logic, connectors, and backend services with separate operating limits. Concurrent load can expose latency or failures in either part of the request path.&lt;/P&gt;
&lt;P&gt;Start by taking a single&amp;nbsp;&lt;STRONG&gt;turn&lt;/STRONG&gt; apart. A turn is one user message and everything the agent does to answer it, up to the moment it signals&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;turn.complete&lt;/SPAN&gt;. The elapsed time spans several parts of the request path, including the Copilot Studio-managed path and external dependencies.&lt;/P&gt;
&lt;img&gt;Figure 1. One turn crosses two zones: the Copilot Studio managed path (green), which scales within its capacity and quota limits, and external dependencies (amber), which have their own limits.&lt;/img&gt;
&lt;P&gt;The green zone is the path managed by Copilot Studio: the Direct Line channel that accepts the message, the enhanced orchestration runtime that reasons over the agent's parts (its model, instructions, knowledge, tools, skills, and connected agents), and the reply that streams back one message at a time. The platform automatically scales to support increases in demand and load while coordinating those parts within those same limits. Constraints in this zone can also contribute latency or failures.&lt;/P&gt;
&lt;P&gt;The amber zone covers everything a turn reaches beyond that managed path. From inside the green zone, the agent's&amp;nbsp;&lt;STRONG&gt;tools&lt;/STRONG&gt;&amp;nbsp;call out to these dependencies: custom logic and Power Automate flows; connectors to systems like Salesforce, ServiceNow, SharePoint, and Dataverse; backend APIs and databases; and hosted&amp;nbsp;&lt;STRONG&gt;MCP servers&lt;/STRONG&gt;. (A2A connections to&amp;nbsp;&lt;STRONG&gt;remote agents&lt;/STRONG&gt; on other platforms existed only for classic agents at the time of this test, so they're out of scope here.) Each dependency has its own configuration and limits. Under load, any part of either zone can add latency or fail; full-agent timings alone do not identify the source.&lt;/P&gt;
&lt;H2&gt;Test each component in isolation&lt;/H2&gt;
&lt;P&gt;Component-level tests help isolate constraints before a full-agent run. Apply representative load directly to the cloud flow, connector, backend API, and MCP server one at a time. The results can show the request rate at which latency rises, throttling appears, or requests begin to fail for that dependency.&lt;/P&gt;
&lt;img&gt;Figure 2. Each dependency is load-tested separately to measure where latency rises, throttling appears, or requests begin to fail.&lt;/img&gt;
&lt;P&gt;An isolated run turns a vague "the agent felt slow" into measured component behavior. A full-agent run can then be compared with those results without assuming in advance which part caused the delay.&lt;/P&gt;
&lt;H2&gt;Why test the complete agent under load&lt;/H2&gt;
&lt;P&gt;Testing each component helps find its limit. But users experience the complete conversation, not one component at a time. The agent may take a different path for each message. It may return one reply or several replies. Some steps may also take much longer than others. This is true even when the agent does not call an external system.&lt;/P&gt;
&lt;P&gt;A load test shows how the complete agent behaves when many users are active. The results help set realistic expectations for response time and reliability:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;More users can mean slower replies.&lt;/STRONG&gt;&amp;nbsp;An agent may respond quickly for one tester but slow down when many conversations run at the same time.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Different conversations take different paths.&lt;/STRONG&gt;&amp;nbsp;Some work starts only after a user makes a certain choice. A one-message test may never reach that work.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;The full answer takes longer than the send request.&lt;/STRONG&gt;&amp;nbsp;Sending a message is only the start. The response time ends when the agent has returned its last reply for the turn.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Real results support better targets.&lt;/STRONG&gt;&amp;nbsp;Measured response times and error rates provide a clear baseline for production planning.&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2&gt;What this post does&lt;/H2&gt;
&lt;P&gt;This post shows how to build a Python load test for a conversational agent built with Copilot Studio using&amp;nbsp;&lt;A href="https://locust.io/" target="_blank" rel="noopener"&gt;Locust&lt;/A&gt;, a Python load-testing framework that simulates concurrent users. The test communicates with the agent through the&amp;nbsp;&lt;STRONG&gt;Direct Line API&lt;/STRONG&gt;, the channel used by the tested client application to exchange messages with the published agent.&lt;/P&gt;
&lt;P&gt;Direct Line uses HTTP requests to get a token, start a conversation, and send a message. Replies arrive over a WebSocket connection. Each Locust virtual user follows this same path, sends a series of messages, and measures the time until the agent completes each turn.&lt;/P&gt;
&lt;P&gt;The same Python workload file runs locally and in&amp;nbsp;&lt;STRONG&gt;Azure Load Testing&lt;/STRONG&gt;&amp;nbsp;without code changes. Environment-specific settings come from&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;locust.conf&lt;/SPAN&gt;&amp;nbsp;locally and&lt;SPAN class="lia-text-color-12"&gt;&amp;nbsp;locust.azure.conf&lt;/SPAN&gt;&amp;nbsp;in Azure. The local validation below uses four virtual users for 15 minutes. The cloud run uses the same two user classes with 16 virtual users for 30 minutes, including text conversations, attachment intake, and selective agent creation.&lt;/P&gt;
&lt;P&gt;In this post, I:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Explain how a conversation works over&amp;nbsp;&lt;STRONG&gt;Direct Line and WebSockets&lt;/STRONG&gt;.&lt;/LI&gt;
&lt;LI&gt;Build the Locust client one step at a time.&lt;/LI&gt;
&lt;LI&gt;Handle complete turns, errors, and file uploads.&lt;/LI&gt;
&lt;LI&gt;Run both conversation paths under load: users describing requirements in chat and users submitting requirements as file attachments.&lt;/LI&gt;
&lt;LI&gt;Run the same test locally and in Azure Load Testing.&lt;/LI&gt;
&lt;/UL&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;STRONG&gt;The agent under test.&lt;/STRONG&gt;&amp;nbsp;The examples use&amp;nbsp;&lt;EM&gt;Automatic Agent Creator&lt;/EM&gt;, a demonstration Copilot Studio agent that reads a business request, asks follow-up questions, and either recommends an integration approach or creates and publishes a new agent. The Direct Line flow can be adapted to other published agents, but conversation behavior, events, and response shapes must be validated for each agent, client, channel configuration, and product version.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;H2&gt;How a Copilot Studio agent talks over Direct Line&lt;/H2&gt;
&lt;P&gt;The tested agent and client used this shape: open a session, exchange messages while it is alive, and read replies until the agent signals that the turn is done. This load test reproduces that flow end to end so its timings include the reply path, not only the message-send request.&lt;/P&gt;
&lt;P&gt;Direct Line can deliver replies through a WebSocket stream or HTTP GET polling. Microsoft's&amp;nbsp;&lt;A href="https://learn.microsoft.com/en-us/microsoft-copilot-studio/guidance/conversational-agents-performance-testing-direct-line" target="_blank" rel="noopener"&gt;performance-testing guidance&lt;/A&gt;&amp;nbsp;says to use WebSockets when the client-facing application uses them; HTTP GET remains available when it does not. The tested client used WebSockets, so this harness does too. The base host in every request below is a Direct Line regional endpoint.&lt;/P&gt;
&lt;H3&gt;Get a token&lt;/H3&gt;
&lt;P&gt;Before a client can start a conversation, it needs a conversation token. A&amp;nbsp;&lt;STRONG&gt;Direct Line secret&lt;/STRONG&gt; is sent to the token endpoint to request one. Direct Line returns a token for one conversation and an&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;expires_in&lt;/SPAN&gt;&amp;nbsp;value that gives the number of seconds until it expires. The client uses this token for the conversation requests that follow.&lt;/P&gt;
&lt;img&gt;Figure 3. The secret is exchanged for a conversation token.&lt;/img&gt;&lt;LI-CODE lang=""&gt;POST  {host}/v3/directline/tokens/generate
Authorization: Bearer «Direct Line secret»

→ 200   { "conversationId": "…", "token": "eyJhbGci…", "expires_in": «seconds until expiry» }&lt;/LI-CODE&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;&amp;nbsp;&lt;/DIV&gt;
&lt;P&gt;The tested conversations completed before their returned token expiry. A longer-lived client must use the returned &lt;SPAN class="lia-text-color-12"&gt;expires_in&lt;/SPAN&gt; value and refresh the token before it expires.&lt;/P&gt;
&lt;H3&gt;Start a conversation and open the socket&lt;/H3&gt;
&lt;P&gt;To start a conversation, the client sends the token to Direct Line in an HTTP request. Direct Line returns two important values: a &lt;SPAN class="lia-text-color-12"&gt;conversationId&lt;/SPAN&gt;, which identifies the conversation, and a &lt;SPAN class="lia-text-color-12"&gt;streamUrl&lt;/SPAN&gt;, which is the WebSocket address used to receive replies. The client opens the WebSocket and keeps it open until the conversation ends.&lt;/P&gt;
&lt;img&gt;Figure 4. Starting the conversation returns a streamUrl; the client connects and holds the socket open.&lt;/img&gt;&lt;LI-CODE lang=""&gt;POST  {host}/v3/directline/conversations
Authorization: Bearer «token»

→ 201   { "conversationId": "…", "streamUrl": "wss://…/stream", "token": "eyJhbGci…" }

WS CONNECT  wss://…/stream        →  socket open (HTTP 101)&lt;/LI-CODE&gt;
&lt;H3&gt;Send a message&lt;/H3&gt;
&lt;P&gt;With the conversation started and the WebSocket open, the client can send the first user message. In Direct Line, a message is represented as an&amp;nbsp;&lt;STRONG&gt;activity&lt;/STRONG&gt;. The client sends this activity through an HTTP POST, while the agent's replies return through the open WebSocket.&lt;/P&gt;
&lt;LI-CODE lang=""&gt;POST  {host}/v3/directline/conversations/{id}/activities
Authorization: Bearer «token»
{ "type": "message", "from": { "id": "user-…" }, "text": "…", "locale": "en-US" }

→ 200   { "id": "…" }&lt;/LI-CODE&gt;
&lt;P&gt;The&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;200&amp;nbsp;&lt;/SPAN&gt;response contains the ID assigned to the activity. It confirms that Direct Line accepted the message, but it does not contain the agent's answer or mean that the agent has finished processing the message. The answer arrives separately through the WebSocket connection.&lt;/P&gt;
&lt;H3&gt;Receive replies until&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;turn.complete&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;After Direct Line accepts the message activity, the agent's replies arrive through the open WebSocket. A turn may contain one agent message or several. When the agent finishes the turn, the stream sends an event named&lt;SPAN class="lia-text-color-12"&gt;&amp;nbsp;turn.complete.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;The client ends the read for the current turn when this event arrives. The WebSocket remains open for the next message.&lt;/P&gt;
&lt;P&gt;Two measurements describe the response time. Although TTFB usually means&amp;nbsp;&lt;EM&gt;time to first byte&lt;/EM&gt;, this harness uses the label for the time to the first complete agent message:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;TTFB&lt;/STRONG&gt;&amp;nbsp;is the time from the start of the send request to the first agent message.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;ResponseTime&lt;/STRONG&gt; is the time from the start of the send request to the last agent message.&lt;/LI&gt;
&lt;/UL&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;STRONG&gt;Note.&lt;/STRONG&gt;&amp;nbsp;In this post, TTFB means the time to the first&amp;nbsp;&lt;EM&gt;complete agent message&lt;/EM&gt;, not the first network byte.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;img&gt;Figure 5. TTFB measures the time from the send to the first complete agent message. ResponseTime measures the time from the send to the last agent message. The turn.complete event ends the turn read.&lt;/img&gt;
&lt;P&gt;Typing activities and the&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;turn.complete&lt;/SPAN&gt;&amp;nbsp;event are not used as timing endpoints. The first and last agent messages set the measurements.&lt;/P&gt;
&lt;P&gt;For a turn with one agent message, TTFB and ResponseTime are equal. When the agent first sends an acknowledgment and later sends the completed result, ResponseTime is longer than TTFB. In the Azure run reported later, the agent-creation turn averaged 8.68 seconds to the first message and 163.71 seconds to completion.&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;STRONG&gt;Note.&lt;/STRONG&gt; Direct Line does not provide a general "last message" marker. The standard performance-testing guidance uses&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;replyToId&amp;nbsp;&lt;/SPAN&gt;to match replies to the sent message and an inactivity timeout to decide when the response has ended. The new agent experience adds&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;turn.complete&lt;/SPAN&gt;&amp;nbsp;as an explicit end-of-turn signal. This client uses that event as the normal stopping point, keeps an overall deadline as a safety check, and uses sender ID because&lt;SPAN class="lia-text-color-12"&gt;&amp;nbsp;from.role&lt;/SPAN&gt;&amp;nbsp;may be missing.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;H2&gt;Implement one virtual-user conversation&lt;/H2&gt;
&lt;P&gt;One virtual user repeats a small cycle: open a Direct Line conversation, send a turn, receive the replies, record the timing, pause, and close. The excerpts below implement that cycle before Locust adds concurrency.&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;STRONG&gt;Note.&lt;/STRONG&gt; These focused excerpts omit some hardening, debug logging, transcript details, and upload internals. The complete client contains them.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;P&gt;The important imports are shown once and reused below:&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;import json          # Python standard library: decode WebSocket frames
import time          # Python standard library: measure turn duration

import websocket     # websocket-client: open and read the WebSocket
from locust import FastHttpUser, between
from locust.exception import StopUser&lt;/LI-CODE&gt;
&lt;P&gt;&lt;SPAN class="lia-text-color-12"&gt;json&lt;/SPAN&gt; and &lt;SPAN class="lia-text-color-12"&gt;time &lt;/SPAN&gt;come from Python. &lt;SPAN class="lia-text-color-12"&gt;websocket &lt;/SPAN&gt;comes from &lt;SPAN class="lia-text-color-12"&gt;websocket-client&lt;/SPAN&gt;. Locust supplies &lt;SPAN class="lia-text-color-12"&gt;FastHttpUser&lt;/SPAN&gt;, &lt;SPAN class="lia-text-color-12"&gt;between&lt;/SPAN&gt;, and &lt;SPAN class="lia-text-color-12"&gt;StopUser&lt;/SPAN&gt;. Uppercase names such as &lt;SPAN class="lia-text-color-12"&gt;REPLY_DEADLINE &lt;/SPAN&gt;are constants in &lt;SPAN class="lia-text-color-12"&gt;new_chat_client.py.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;Open the session:&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;connect()&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;At the top of every task, &lt;SPAN class="lia-text-color-12"&gt;connect()&lt;/SPAN&gt; gives the virtual user a unique id, gets a token, starts a conversation, and opens the WebSocket. The unique id matters later: it's how the client tells the agent's replies apart from its own echoed message.&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;def connect(self):
    """Get a token, start a conversation, and open the WebSocket."""
    self._user_id = "user-" + str(id(self))
    self._turn = 0
    self._token = self._get_token()
    if not self._token:
        raise StopUser()
    started = self._start_conversation()
    if not started:
        raise StopUser()
    self.conversation_id, stream_url = started
    self._ws = websocket.create_connection(stream_url, timeout=WS_CONNECT_TIMEOUT)
    self._ws.settimeout(WS_RECV_TIMEOUT)   # each recv() polls for at most a second&lt;/LI-CODE&gt;
&lt;P&gt;The token and start calls are ordinary HTTP, wrapped so Locust records each one and marks a bad status as a failure.&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;def _get_token(self):
    url = self.directline + "/v3/directline/tokens/generate"
    headers = {"Authorization": "Bearer " + self.direct_line_secret}
    with self.client.post(url, headers=headers, name=self.label + " token",
                          catch_response=True) as response:
        if response.status_code != 200:
            response.failure("token HTTP " + str(response.status_code))
            return None
        body = parse_json(response)
        if not body or not body.get("token"):
            response.failure("token response missing 'token'")
            return None
        return body["token"]

def _start_conversation(self):
    url = self.directline + "/v3/directline/conversations"
    with self.client.post(url, headers=self._auth_header(),
                          name=self.label + " start", catch_response=True) as response:
        if response.status_code not in (200, 201):
            response.failure("start HTTP " + str(response.status_code))
            return None
        body = parse_json(response) or {}
        conversation_id = body.get("conversationId")
        stream_url = body.get("streamUrl")
        if not conversation_id or not stream_url:
            response.failure("start response missing conversationId or streamUrl")
            return None
        if body.get("token"):
            self._token = body["token"]
        return conversation_id, stream_url&lt;/LI-CODE&gt;
&lt;H3&gt;One turn:&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;say()&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN class="lia-text-color-12"&gt;say()&lt;/SPAN&gt; owns one turn: start the clock, send the activity, receive replies, record both timings, and return the last non-empty agent message. &lt;SPAN class="lia-text-color-12"&gt;metric_name&lt;/SPAN&gt; labels the Locust rows, &lt;SPAN class="lia-text-color-12"&gt;deadline &lt;/SPAN&gt;limits the whole turn, and &lt;SPAN class="lia-text-color-12"&gt;attach &lt;/SPAN&gt;selects the upload path.&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;def say(self, text, metric_name=None, deadline=REPLY_DEADLINE, attach=None):
    """Send one message, wait for the reply, and return the reply text."""
    self._turn += 1
    start = time.perf_counter()
    posted_id = self._send(text, attach)
    if posted_id is None:
        self._record(start, None, None, metric_name, "", "send failed")
        raise StopUser()
    result = self._receive(start, deadline)
    self._record(start, result.t_first, result.t_final, metric_name,
                 result.text, result.error)
    if result.error:
        raise StopUser()
    return result.text&lt;/LI-CODE&gt;
&lt;P&gt;Earlier replies still set the timing boundaries, but&lt;SPAN class="lia-text-color-12"&gt; result.text&lt;/SPAN&gt; contains only the last non-empty message.&lt;SPAN class="lia-text-color-12"&gt; _send()&lt;/SPAN&gt; posts a normal message unless &lt;SPAN class="lia-text-color-12"&gt;attach &lt;/SPAN&gt;selects the upload path explained later.&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;def _send(self, text, attach=None):
    if attach:
        return self._send_file(text, attach)  # explained in the attachment section
    activity = {"type": "message", "from": {"id": self._user_id},
                "text": text, "locale": "en-US"}
    url = (self.directline + "/v3/directline/conversations/" +
           self.conversation_id + "/activities")
    with self.client.post(url, json=activity, headers=self._auth_header(),
                          name=self.label + " send", catch_response=True) as response:
        if response.status_code != 200:
            response.failure("send HTTP " + str(response.status_code))
            return None
        body = parse_json(response)
        if not body or not body.get("id"):
            response.failure("send response missing activity id")
            return None
        return body["id"]&lt;/LI-CODE&gt;
&lt;P&gt;The returned activity ID confirms acceptance, not an answer. The answer arrives on the WebSocket, while Locust records the POST as a separate &lt;SPAN class="lia-text-color-12"&gt;send &lt;/SPAN&gt;row.&lt;/P&gt;
&lt;H3&gt;Read WebSocket frames until the turn ends&lt;/H3&gt;
&lt;P&gt;&lt;SPAN class="lia-text-color-12"&gt;_receive() &lt;/SPAN&gt;reads JSON frames from the open socket. &lt;SPAN class="lia-text-color-12"&gt;TurnResult &lt;/SPAN&gt;keeps the latest text, the first and final message times, and any error:&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;class TurnResult:
    def __init__(self):
        self.text = ""       # last non-empty agent message
        self.t_first = None  # first agent-message time
        self.t_final = None  # latest agent-message time
        self.error = ""    # non-empty when the turn fails&lt;/LI-CODE&gt;
&lt;P&gt;The one-second socket timeout keeps each read responsive; the overall deadline limits the complete turn, including the HTTP send.&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;def _receive(self, start, max_wait=REPLY_DEADLINE):
    """Read frames until the agent signals 'turn.complete' or the deadline passes."""
    result = TurnResult()
    deadline = start + max_wait
    while time.perf_counter() &amp;lt; deadline:
        try:
            frame = self._ws.recv()
        except websocket.WebSocketTimeoutException:
            continue                      # no data this second; keep waiting
        if not frame or not frame.strip():
            continue
        payload = json.loads(frame)
        for activity in payload.get("activities", []):
            if self._handle_activity(activity, result):
                return self._finish(result)   # saw turn.complete
    if result.t_final is None and not result.error:
        result.error = "no final reply (timeout)"
    return self._finish(result)&lt;/LI-CODE&gt;
&lt;P&gt;Each activity has one job:&lt;/P&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;&lt;table class="lia-border-style-double" border="1" style="border-width: 1px;"&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Activity&lt;/th&gt;&lt;th&gt;Client action&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;User&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;message&amp;nbsp;&lt;/SPAN&gt;echo&lt;/td&gt;&lt;td&gt;Ignore it because it came from the virtual user&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;SPAN class="lia-text-color-12"&gt;typing&lt;/SPAN&gt;&lt;/td&gt;&lt;td&gt;Ignore it for response-time measurements&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Agent&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;message&lt;/SPAN&gt;&lt;/td&gt;&lt;td&gt;Set the first and latest message times; keep the latest non-empty text&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;SPAN class="lia-text-color-12"&gt;trace&amp;nbsp;&lt;/SPAN&gt;with an&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;ErrorCode&lt;/SPAN&gt;&lt;/td&gt;&lt;td&gt;Store the structured turn error&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;event&amp;nbsp;named&lt;SPAN class="lia-text-color-12"&gt;&amp;nbsp;turn.complete&lt;/SPAN&gt;&lt;/td&gt;&lt;td&gt;End the read for this turn; keep the WebSocket open&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;colgroup&gt;&lt;col style="width: 50.00%" /&gt;&lt;col style="width: 50.00%" /&gt;&lt;/colgroup&gt;&lt;/table&gt;&lt;/DIV&gt;
&lt;P&gt;&lt;SPAN class="lia-text-color-12"&gt;_handle_activity()&lt;/SPAN&gt; applies the table. &lt;SPAN class="lia-text-color-12"&gt;_is_bot_reply()&lt;/SPAN&gt; filters the user echo and is explained next.&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;def _handle_activity(self, activity, result):
    activity_type = activity.get("type")
    if activity_type == "message" and self._is_bot_reply(activity):
        now = time.perf_counter()
        if result.t_first is None:
            result.t_first = now          # first reply  -&amp;gt; TTFB
        result.t_final = now              # every reply  -&amp;gt; ResponseTime
        result.text = activity.get("text") or result.text
        return False
    if activity_type == "trace":
        code = self._error_code(activity)
        if code and not result.error:
            result.error = "bot error: " + describe_error_code(code)
        return False
    if activity_type == "event" and activity.get("name") == "turn.complete":
        return True
    return False&lt;/LI-CODE&gt;
&lt;P&gt;Normally &lt;SPAN class="lia-text-color-12"&gt;turn.complete&lt;/SPAN&gt; ends the read. The deadline is the fallback; a turn with no agent message fails.&lt;/P&gt;
&lt;H3&gt;Record the latency&lt;/H3&gt;
&lt;P&gt;&lt;SPAN class="lia-text-color-12"&gt;_record() &lt;/SPAN&gt;emits TTFB and ResponseTime for a successful turn. A failed turn emits one failed ResponseTime entry instead of a latency value.&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;def _record(self, start, t_first, t_final, metric_name, text, error):
    name = self.label + " " + (metric_name or ("t" + str(self._turn)))
    fire = self.environment.events.request.fire
    if error:
        fire(request_type="CHAT", name=name + " [ResponseTime]", response_time=None,
             response_length=0, exception=Exception(error), context={})
        return
    fire(request_type="CHAT", name=name + " [TTFB]",
         response_time=((t_first or t_final) - start) * 1000, response_length=0,
         exception=None, context={})
    fire(request_type="CHAT", name=name + " [ResponseTime]",
         response_time=(t_final - start) * 1000, response_length=len(text),
         exception=None, context={})&lt;/LI-CODE&gt;
&lt;H3&gt;Pause between turns and close the conversation&lt;/H3&gt;
&lt;P&gt;The scenario pauses between messages and closes the WebSocket in &lt;SPAN class="lia-text-color-12"&gt;finally&lt;/SPAN&gt;, even when a turn fails.&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;try:
    self.connect()
    self.say(requirement, "T01 requirement")
    self.think(20, 30)
    self.say("ok", "T02 confirm")
finally:
    self.close()&lt;/LI-CODE&gt;
&lt;P&gt;That completes one conversation.&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;STRONG&gt;Note.&lt;/STRONG&gt; This is a trimmed two-turn illustration (&lt;SPAN class="lia-text-color-12"&gt;T01 requirement&lt;/SPAN&gt;&amp;nbsp;then&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;T02 confirm&lt;/SPAN&gt;). The demo text scenario reported later runs five turns:&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;T01 requirement,&lt;/SPAN&gt;&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;T02 source&lt;/SPAN&gt;,&lt;SPAN class="lia-text-color-12"&gt;&amp;nbsp;T03 target&lt;/SPAN&gt;,&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;T04 action&lt;/SPAN&gt;&amp;nbsp;(only some conversations reach this branch), and&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;T05 confirm&lt;/SPAN&gt;.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;H3&gt;Assemble the reusable Locust user&lt;/H3&gt;
&lt;P&gt;In &lt;SPAN class="lia-text-color-12"&gt;new_chat_client.py&lt;/SPAN&gt;, the methods above belong to &lt;SPAN class="lia-text-color-12"&gt;WebSocketChatClient&lt;/SPAN&gt;. The class extends &lt;SPAN class="lia-text-color-12"&gt;FastHttpUser &lt;/SPAN&gt;and holds their shared configuration:&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;class WebSocketChatClient(FastHttpUser):
    """Direct Line WebSocket load client for a new-experience Copilot Studio agent."""
    abstract = True
    host = DIRECTLINE
    wait_time = between(2, 6)

    directline = DIRECTLINE
    direct_line_secret = None   # secret mode
    label = "ws"&lt;/LI-CODE&gt;
&lt;P&gt;&lt;SPAN class="lia-text-color-12"&gt;abstract = True &lt;/SPAN&gt;prevents Locust from running the base directly. A scenario subclass supplies the secret, metric label, and messages. &lt;SPAN class="lia-text-color-12"&gt;wait_time &lt;/SPAN&gt;pauses between complete scenario runs, while explicit &lt;SPAN class="lia-text-color-12"&gt;think() &lt;/SPAN&gt;calls pause between turns. Locust can then create many scenario instances that reuse the same conversation methods.&lt;/P&gt;
&lt;H2&gt;Handling the new experience&lt;/H2&gt;
&lt;P&gt;Three behaviors of the new agent experience would quietly break a naive client. Each is a few lines in the methods above.&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;STRONG&gt;Preview notice.&lt;/STRONG&gt;&amp;nbsp;As of July 13, 2026, the&amp;nbsp;&lt;A href="https://learn.microsoft.com/en-us/microsoft-copilot-studio/agents-experience/overview" target="_blank" rel="noopener"&gt;Copilot Studio new agent experience&lt;/A&gt;&amp;nbsp;is a production-ready preview. Microsoft identifies its documentation as prerelease and subject to change, and states that production-ready previews are subject to the&amp;nbsp;&lt;A href="https://go.microsoft.com/fwlink/?linkid=2189520" target="_blank" rel="noopener"&gt;Supplemental Terms of Use for Microsoft Azure Previews&lt;/A&gt;.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;H3&gt;Replies may omit a role&lt;/H3&gt;
&lt;P&gt;In the classic channel, an agent message carries &lt;SPAN class="lia-text-color-12"&gt;from.role = "bot"&lt;/SPAN&gt;. In the new experience some replies arrive with only &lt;SPAN class="lia-text-color-12"&gt;from.id&lt;/SPAN&gt; and no role at all. Keying off&lt;SPAN class="lia-text-color-12"&gt; role == "bot" &lt;/SPAN&gt;would drop those messages and report "no reply." The fix treats any message that isn't the client's own echo as a reply:&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;def _is_bot_reply(self, activity):
    """True if the message is from the agent (role may be missing), not the client's echo."""
    sender = activity.get("from") or {}
    role = sender.get("role")
    if role == "bot":
        return True
    if role == "user":
        return False
    return sender.get("id") != self._user_id   # role missing -&amp;gt; not the echo -&amp;gt; a reply&lt;/LI-CODE&gt;
&lt;H3&gt;&lt;SPAN class="lia-text-color-12"&gt;turn.complete&lt;/SPAN&gt;&amp;nbsp;is an explicit end-of-turn event&lt;/H3&gt;
&lt;P&gt;Reading it (rather than waiting out a timeout) is what lets a fast turn finish in a couple of seconds instead of idling. It's the &lt;SPAN class="lia-text-color-12"&gt;event &lt;/SPAN&gt;branch in &lt;SPAN class="lia-text-color-12"&gt;_handle_activity()&lt;/SPAN&gt; above.&lt;/P&gt;
&lt;H3&gt;Errors come back as a structured code&lt;/H3&gt;
&lt;P&gt;When something goes wrong, the agent can send a&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;trace&amp;nbsp;&lt;/SPAN&gt;activity carrying a locale-independent&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;ErrorCode&lt;/SPAN&gt;. The client maps it against the official code list so a run reports&amp;nbsp;&lt;EM&gt;why&lt;/EM&gt; it failed, not just that a reply never came:&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;def _error_code(self, activity):
    if activity.get("valueType") != "ErrorCode":
        return None
    value = activity.get("value")
    if isinstance(value, dict) and value.get("ErrorCode"):
        return value["ErrorCode"]
    return "error"&lt;/LI-CODE&gt;
&lt;H2&gt;Attach files with a multipart upload&lt;/H2&gt;
&lt;P&gt;A file-reading turn exercises work that a text-only turn does not. This harness sends one or more files through the Direct Line&lt;SPAN class="lia-text-color-12"&gt;&amp;nbsp;/upload&lt;/SPAN&gt;&amp;nbsp;endpoint as&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;multipart/form-data&lt;/SPAN&gt;, with an optional message activity in the same request.&lt;/P&gt;
&lt;H3&gt;The multipart request&lt;/H3&gt;
&lt;P&gt;The request contains one activity part and one file part per attachment:&lt;/P&gt;
&lt;LI-CODE lang=""&gt;POST {host}/v3/directline/conversations/{id}/upload?userId={from.id}
Authorization: Bearer «token»
Content-Type: multipart/form-data; boundary=----loadtest-«random»

------loadtest-«random»
Content-Disposition: form-data; name="activity"
Content-Type: application/vnd.microsoft.activity

{ "type": "message", "from": { "id": "user-…" }, "text": "" }
------loadtest-«random»
Content-Disposition: form-data; name="file"; filename="requirement.csv"
Content-Type: text/csv

«raw file bytes»
------loadtest-«random»--

→ 200   { "id": "…" }        (same shape as a normal send)&lt;/LI-CODE&gt;
&lt;P&gt;The required&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;userId&amp;nbsp;&lt;/SPAN&gt;query parameter identifies the sender. The harness uses the same per-instance ID in&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;userId&amp;nbsp;&lt;/SPAN&gt;and&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;activity.from.id&lt;/SPAN&gt;, so the echoed activity has the sender ID expected by the reply filter.&lt;/P&gt;
&lt;P&gt;The activity JSON contains no &lt;SPAN class="lia-text-color-12"&gt;attachments &lt;/SPAN&gt;array. Direct Line adds the separate file parts as attachments to that activity before sending it to the agent. A successful upload returns the same&lt;SPAN class="lia-text-color-12"&gt; { "id": "…" }&lt;/SPAN&gt; shape as a text send, so the existing WebSocket receive and timing path remains unchanged.&lt;/P&gt;
&lt;H3&gt;Building the body by hand&lt;/H3&gt;
&lt;P&gt;Locust's&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;FastHttpUser&amp;nbsp;&lt;/SPAN&gt;has no explicit requests-style&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;files=&lt;/SPAN&gt;&amp;nbsp;helper. The client therefore assembles the multipart body as bytes. A fresh UUID-based boundary is used for each request, every file is read before the POST begins, and each attachment gets its own&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;file&amp;nbsp;&lt;/SPAN&gt;part.&lt;/P&gt;
&lt;P&gt;The implementation uses four additional standard-library modules:&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;import mimetypes
import os
import urllib.parse
import uuid&lt;/LI-CODE&gt;&lt;LI-CODE lang="python"&gt;def _send_file(self, text, attach):
    """Upload one or more files (optionally with a message) via Direct Line /upload."""
    paths = attach if isinstance(attach, list) else [attach]

    files = []                                   # read every file first
    for path in paths:
        with open(path, "rb") as handle:
            data = handle.read()
        name = os.path.basename(path)
        extension = os.path.splitext(path)[1].lower()
        mime = UPLOAD_MIME.get(extension) or mimetypes.guess_type(path)[0] or "application/octet-stream"
        files.append((name, mime, data))

    activity = json.dumps({"type": "message", "from": {"id": self._user_id}, "text": text or ""})
    boundary = "----loadtest-" + uuid.uuid4().hex   # fresh boundary per request
    dash = ("--" + boundary).encode("utf-8")
    parts = [
        dash + b"\r\n",
        b'Content-Disposition: form-data; name="activity"\r\n',
        b"Content-Type: application/vnd.microsoft.activity\r\n\r\n",
        activity.encode("utf-8") + b"\r\n",
    ]
    for name, mime, data in files:                # one part per file
        disposition = _content_disposition("file", name)
        parts.append(dash + b"\r\n")
        parts.append(("Content-Disposition: " + disposition + "\r\n").encode("utf-8"))
        parts.append(("Content-Type: " + mime + "\r\n\r\n").encode("utf-8"))
        parts.append(data + b"\r\n")
    parts.append(dash + b"--\r\n")

    url = (self.directline + "/v3/directline/conversations/" +
           self.conversation_id + "/upload?userId=" + self._user_id)
    headers = self._auth_header()
    headers["Content-Type"] = "multipart/form-data; boundary=" + boundary
    with self.client.post(url, data=b"".join(parts), headers=headers,
                          name=self.label + " upload", catch_response=True) as response:
        if response.status_code != 200:
            response.failure("upload HTTP " + str(response.status_code))
            return None
        posted = parse_json(response)
        if not posted or not posted.get("id"):
            response.failure("upload response missing activity id")
            return None
        return posted["id"]&lt;/LI-CODE&gt;
&lt;P&gt;The media type comes from a small known-types table, then Python's &lt;SPAN class="lia-text-color-12"&gt;mimetypes&lt;/SPAN&gt;, then&lt;SPAN class="lia-text-color-12"&gt; application/octet-stream&lt;/SPAN&gt;. This labels unknown extensions without claiming that every file type or size can be processed by the agent. The client sends the basename in the multipart header; exact preservation of non-ASCII filenames is not assumed.&lt;/P&gt;
&lt;H3&gt;Use the same turn API&lt;/H3&gt;
&lt;P&gt;Scenarios continue to call&lt;SPAN class="lia-text-color-12"&gt;&amp;nbsp;say(...)&lt;/SPAN&gt;.&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;_send()&amp;nbsp;&lt;/SPAN&gt;chooses the ordinary message endpoint or multipart&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;/upload&lt;/SPAN&gt;:&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;def _send(self, text, attach=None):
    if attach:
        return self._send_file(text, attach)   # multipart /upload
    # … otherwise the ordinary text Send Activity from earlier&lt;/LI-CODE&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Text only&lt;/STRONG&gt;&amp;nbsp;— the ordinary Send Activity.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Text and a file&lt;/STRONG&gt;&amp;nbsp;— a message plus one attachment.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;A file with no message&lt;/STRONG&gt;&amp;nbsp;— pass&amp;nbsp;text=""&amp;nbsp;with an attachment.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Several files&lt;/STRONG&gt; — pass a list, and each becomes its own&amp;nbsp;file&amp;nbsp;part in one upload.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Because &lt;SPAN class="lia-text-color-12"&gt;/upload&lt;/SPAN&gt; returns the same activity-ID shape as a text send, an upload turn is received and timed by the same &lt;SPAN class="lia-text-color-12"&gt;say()&lt;/SPAN&gt; path:&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;# A conversation that hands the agent a requirements file instead of typing it
self.say("Here is my requirement", "T01 requirement", attach="requirement_intake.csv")&lt;/LI-CODE&gt;
&lt;H2&gt;Validate the workload locally first&lt;/H2&gt;
&lt;P&gt;Before running the larger test in Azure, the workload ran locally in one Python process with&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;locust.conf&lt;/SPAN&gt;: four virtual users for 15 minutes, starting one user every 30 seconds. This verified both conversation paths, file uploads, pacing, transaction names, and diagnostics. Azure Load Testing then used the same Python workload with&lt;SPAN class="lia-text-color-12"&gt;&amp;nbsp;locust.azure.conf&lt;/SPAN&gt;, increasing the profile to 16 virtual users for 30 minutes and omitting local result paths.&lt;/P&gt;
&lt;P&gt;The Python environment used three dependencies beyond the standard library:&lt;/P&gt;
&lt;LI-CODE lang=""&gt;python -m pip install "locust==2.42.6" "python-dotenv&amp;gt;=1.0,&amp;lt;2.0" "websocket-client==1.9.0"&lt;/LI-CODE&gt;
&lt;P&gt;The Direct Line secret came from an environment variable in a local &lt;SPAN class="lia-text-color-12"&gt;.env&lt;/SPAN&gt; file, which kept it out of source control:&lt;/P&gt;
&lt;LI-CODE lang=""&gt;DL_IA_SECRET=&amp;lt;Direct Line secret&amp;gt;&lt;/LI-CODE&gt;
&lt;P&gt;The local run-time included the ramp period. The complete profile lived in &lt;SPAN class="lia-text-color-12"&gt;locust.conf&lt;/SPAN&gt;:&lt;/P&gt;
&lt;LI-CODE lang=""&gt;locustfile   = locustfile_discovery_demo.py
headless     = true
users        = 4
spawn-rate   = 0.0333333333
run-time     = 15m
stop-timeout = 1200
only-summary = true
csv          = results/local-15m
html         = results/local-15m.html&lt;/LI-CODE&gt;
&lt;P&gt;The local launch was then two lines:&lt;/P&gt;
&lt;LI-CODE lang=""&gt;New-Item -ItemType Directory -Force results | Out-Null
python -m locust --config locust.conf&lt;/LI-CODE&gt;
&lt;P&gt;The 1,200-second stop timeout was an upper bound, not a fixed extension. When the 15-minute window closed, Locust allowed an in-flight conversation to complete instead of interrupting a turn. This mattered because the optional agent-creation turn had a 300-second reply deadline. Each completed conversation wrote a readable transcript under&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;transcripts/&lt;/SPAN&gt;; frame-level JSONL was written under&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;transcripts/directline-debug/&lt;/SPAN&gt;&amp;nbsp;because&lt;SPAN class="lia-text-color-12"&gt;&amp;nbsp;DL_DEBUG_LOG&lt;/SPAN&gt;&amp;nbsp;was enabled.&lt;/P&gt;
&lt;P&gt;Locust reported the token, conversation-start, send, and upload HTTP calls alongside the&lt;SPAN class="lia-text-color-12"&gt;&amp;nbsp;[TTFB]&lt;/SPAN&gt;&amp;nbsp;and&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;[ResponseTime]&amp;nbsp;&lt;/SPAN&gt;chat measurements. Transaction names described logical steps rather than individual system combinations, which kept route variants in the same result rows.&lt;/P&gt;
&lt;P&gt;The local validation completed successfully. Both conversation paths, file uploads, and optional agent creation finished with zero failures or exceptions.&lt;/P&gt;
&lt;H2&gt;Scale the test in Azure Load Testing&lt;/H2&gt;
&lt;P&gt;The Azure test used one engine to run 16 virtual users for 30 minutes. Eight users followed the text conversation scenario and eight followed the file-attachment scenario. Locust started one user every 30 seconds and kept the configured 20–30 second pause between messages.&lt;/P&gt;
&lt;img&gt;Figure 6. One Azure Load Testing engine runs two conversation scenarios: eight users follow the text path and eight follow the file-attachment path for 30 minutes.&lt;/img&gt;
&lt;P&gt;Upload the test files to an Azure Load Testing resource. The YAML disables client-generated transcripts and JSONL files because Azure Load Testing publishes only its supported artifacts: engine logs, raw result CSV, and a dashboard report.&lt;/P&gt;
&lt;LI-CODE lang="yaml"&gt;version: v0.1
testId: copilot-studio-directline-load
displayName: Copilot Studio Direct Line load test
description: Load test with text and file conversation scenarios
testPlan: locustfile_discovery_demo.py
testType: Locust
engineInstances: 1

configurationFiles:
    - new_chat_client.py
    - requirements.txt
    - test_attachment/high_route_requirement.docx
    - test_attachment/high_route_requirement.pdf
    - test_attachment/high_route_requirement.png

properties:
    userPropertyFile: locust.azure.conf

env:
    - { name: LOCUST_USERS, value: "16" }
    - { name: LOCUST_SPAWN_RATE, value: "0.0333333333" }
    - { name: LOCUST_RUN_TIME, value: "1800" }
    - { name: LOCUST_STOP_TIMEOUT, value: "1200" }
    - { name: TEXT_USERS, value: "8" }
    - { name: FILE_USERS, value: "8" }
    - { name: ATTACHMENT_DIR, value: "." }
    - { name: DL_TRANSCRIPT, value: "0" }
    - { name: DL_DEBUG_LOG, value: "0" }

failureCriteria:
    - percentage(error) &amp;gt; 0&lt;/LI-CODE&gt;
&lt;P&gt;Before creating the test, store the Direct Line secret in Azure Key Vault, enable the Azure Load Testing resource's system-assigned managed identity, and grant that identity permission to read the secret. The&amp;nbsp;&lt;A href="https://learn.microsoft.com/en-us/azure/load-testing/how-to-parameterize-load-tests" target="_blank" rel="noopener"&gt;Azure Load Testing secret guidance&lt;/A&gt; covers the identity and Key Vault access steps. In the current Azure CLI, the literal value&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;null&amp;nbsp;&lt;/SPAN&gt;(not an omitted or empty argument) tells&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;--keyvault-reference-id&lt;/SPAN&gt;&amp;nbsp;to use the load-testing resource's own system-assigned identity.&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;STRONG&gt;Azure CLI preview.&lt;/STRONG&gt;&amp;nbsp;The&lt;SPAN class="lia-text-color-12"&gt;&amp;nbsp;az load test&lt;/SPAN&gt;&amp;nbsp;and&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;az load test-run&lt;/SPAN&gt;&amp;nbsp;commands need the&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;load&amp;nbsp;&lt;/SPAN&gt;extension and Azure CLI 2.66.0 or later (currently in preview). Check the current&amp;nbsp;&lt;A href="https://learn.microsoft.com/en-us/cli/azure/load/test?view=azure-cli-latest" target="_blank" rel="noopener"&gt;Azure CLI load test reference&lt;/A&gt; before running them.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;P&gt;Create the test definition from the YAML and set that Key Vault reference identity:&lt;/P&gt;
&lt;LI-CODE lang=""&gt;az load test create `
    --load-test-resource "&amp;lt;load-test-resource&amp;gt;" `
    --resource-group "&amp;lt;resource-group&amp;gt;" `
    --test-id copilot-studio-directline-load `
    --load-test-config-file azure-loadtest.yaml `
    --keyvault-reference-id null&lt;/LI-CODE&gt;
&lt;P&gt;For a new run, pass the Key Vault secret identifier through Azure Load Testing's dedicated &lt;SPAN class="lia-text-color-12"&gt;--secret &lt;/SPAN&gt;parameter. Locust receives a configured secret as an environment variable with the same name, so the Python workload can continue to read&lt;SPAN class="lia-text-color-12"&gt; DL_IA_SECRET&lt;/SPAN&gt; without code changes:&lt;/P&gt;
&lt;LI-CODE lang=""&gt;$runId = "copilot-azure-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
$directLineSecretUri = "https://&amp;lt;key-vault-name&amp;gt;.vault.azure.net/secrets/&amp;lt;secret-name&amp;gt;"
$runEnv = @(
    "LOCUST_USERS=16"
    "LOCUST_SPAWN_RATE=0.0333333333"
    "LOCUST_RUN_TIME=1800"
    "LOCUST_STOP_TIMEOUT=1200"
    "TEXT_USERS=8"
    "FILE_USERS=8"
    "ATTACHMENT_DIR=."
    "DL_TRANSCRIPT=0"
    "DL_DEBUG_LOG=0"
)
$runSecrets = @("DL_IA_SECRET=$directLineSecretUri")

az load test-run create `
    --load-test-resource "&amp;lt;load-test-resource&amp;gt;" `
    --resource-group "&amp;lt;resource-group&amp;gt;" `
    --test-id copilot-studio-directline-load `
    --test-run-id $runId `
    --env $runEnv `
    --secret $runSecrets `
    --only-show-errors `
    --output none&lt;/LI-CODE&gt;
&lt;P&gt;Azure's test-run debug mode was deliberately left off. Debug-mode runs are capped at 10 minutes regardless of the configured Locust duration.&lt;/P&gt;
&lt;P&gt;After the run command completes, download the engine logs, raw results, and dashboard report:&lt;/P&gt;
&lt;LI-CODE lang=""&gt;az load test-run download-files `
    --load-test-resource "&amp;lt;load-test-resource&amp;gt;" `
    --resource-group "&amp;lt;resource-group&amp;gt;" `
    --test-run-id $runId `
    --path "results/azure/$runId" `
    --log --result --report --force&lt;/LI-CODE&gt;
&lt;P&gt;The download command creates&lt;SPAN class="lia-text-color-12"&gt; logs.zip&lt;/SPAN&gt;, &lt;SPAN class="lia-text-color-12"&gt;csv.zip&lt;/SPAN&gt;, and &lt;SPAN class="lia-text-color-12"&gt;reports.zip&lt;/SPAN&gt; in the target directory.&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;STRONG&gt;Security note.&lt;/STRONG&gt; The command passes a Key Vault secret identifier, not the Direct Line secret value. Azure Load Testing stores the identifier, retrieves the secret with the configured managed identity for each run, and exposes it to the Locust process as&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;DL_IA_SECRET&lt;/SPAN&gt;. Keep&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;--env&lt;/SPAN&gt;&amp;nbsp;for non-sensitive settings only. The 2026-07-12 Demo Run passed the secret through&lt;SPAN class="lia-text-color-12"&gt;&amp;nbsp;--env&lt;/SPAN&gt;, which can expose it in run metadata; the recommended command above avoids that. In CI/CD, the Azure Load Testing task or action can instead receive the value through its&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;secrets&amp;nbsp;&lt;/SPAN&gt;input from the pipeline's secret store.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;H2&gt;What the 30-minute Demo Run produced&lt;/H2&gt;
&lt;P&gt;The completed Azure execution is referred to below as the&amp;nbsp;&lt;STRONG&gt;Demo Run&lt;/STRONG&gt;. It ran on 2026-07-12 using a single Azure Load Testing engine, with 16 virtual users split evenly between the text and file paths. Treat the Demo Run as a baseline at this load, not a capacity ceiling.&lt;/P&gt;
&lt;P&gt;The Demo Run completed with a&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;PASSED&amp;nbsp;&lt;/SPAN&gt;verdict and no service error details.&lt;/P&gt;
&lt;P&gt;The Locust engine ran its configured 30-minute window, then allowed in-flight conversations to finish. It reached all 16 users after 7 minutes 30 seconds, then held exactly 16 for the rest of the run. It hit the run-time limit at&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;10:23:22Z&lt;/SPAN&gt;, a steady window of about 22 minutes 30 seconds, and exited cleanly about 1 minute 50 seconds later.&lt;/P&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;&lt;table class="lia-border-style-solid" border="1" style="border-width: 1px;"&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Measure&lt;/th&gt;&lt;th&gt;Result&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Virtual users&lt;/td&gt;&lt;td&gt;16: 8 text and 8 file&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Locust run-time window&lt;/td&gt;&lt;td&gt;30 minutes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Full-load window&lt;/td&gt;&lt;td&gt;About 22 minutes 30 seconds&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Completed conversations&lt;/td&gt;&lt;td&gt;253&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Text conversations&lt;/td&gt;&lt;td&gt;98&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;File conversations&lt;/td&gt;&lt;td&gt;155&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Recorded request samples&lt;/td&gt;&lt;td&gt;3,212&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Failed samples&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Completed agent-creation turns&lt;/td&gt;&lt;td&gt;5&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;colgroup&gt;&lt;col style="width: 50.00%" /&gt;&lt;col style="width: 50.00%" /&gt;&lt;/colgroup&gt;&lt;/table&gt;&lt;/DIV&gt;
&lt;P&gt;The 3,212 samples include Direct Line HTTP calls plus the custom&lt;SPAN class="lia-text-color-12"&gt; [TTFB]&lt;/SPAN&gt; and &lt;SPAN class="lia-text-color-12"&gt;[ResponseTime]&lt;/SPAN&gt; entries emitted for chat turns. Every text conversation that started reached &lt;SPAN class="lia-text-color-12"&gt;T05 confirm&lt;/SPAN&gt;, and every file conversation reached&lt;SPAN class="lia-text-color-12"&gt; T03 confirm.&lt;/SPAN&gt;&lt;/P&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;&lt;table class="lia-border-style-solid" border="1" style="border-width: 1px;"&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Path&lt;/th&gt;&lt;th&gt;Conversations&lt;/th&gt;&lt;th&gt;Recorded samples&lt;/th&gt;&lt;th&gt;Failures&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Text discovery&lt;/td&gt;&lt;td&gt;98&lt;/td&gt;&lt;td&gt;1,507&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;File attachment&lt;/td&gt;&lt;td&gt;155&lt;/td&gt;&lt;td&gt;1,705&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;STRONG&gt;Total&lt;/STRONG&gt;&lt;/td&gt;&lt;td&gt;&lt;STRONG&gt;253&lt;/STRONG&gt;&lt;/td&gt;&lt;td&gt;&lt;STRONG&gt;3,212&lt;/STRONG&gt;&lt;/td&gt;&lt;td&gt;&lt;STRONG&gt;0&lt;/STRONG&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;colgroup&gt;&lt;col style="width: 25.00%" /&gt;&lt;col style="width: 25.00%" /&gt;&lt;col style="width: 25.00%" /&gt;&lt;col style="width: 25.00%" /&gt;&lt;/colgroup&gt;&lt;/table&gt;&lt;/DIV&gt;
&lt;P&gt;Azure Load Testing packages an offline dashboard with per-minute charts, sampler statistics, and error details. The download command in the previous section saves it as&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;reports.zip&lt;/SPAN&gt;. Extract the archive and open&lt;SPAN class="lia-text-color-12"&gt;&amp;nbsp;reports/index.html&lt;/SPAN&gt;. For the Locust-based Demo Run, the downloaded dashboard is also published as the&amp;nbsp;&lt;A href="https://kroy92.github.io/copilot-studio-load-testing/demo-run/" target="_blank" rel="noopener"&gt;Demo Run report&lt;/A&gt; and can be viewed directly. The two cards below summarize latency for the text and file scenarios. Teal shows average TTFB, blue shows average ResponseTime, and orange extends from the average ResponseTime to p90.&lt;/P&gt;
&lt;H3&gt;Text conversation latency&lt;/H3&gt;
&lt;img&gt;Figure 7. Text discovery: 98 conversations, 1,507 recorded samples, eight virtual users, and zero errors. Agent creation uses an independent scale so the regular turns remain readable. Each section uses its own scale, so bar lengths compare within a section, not across sections.&lt;/img&gt;
&lt;P&gt;&lt;STRONG&gt;Observation.&lt;/STRONG&gt;&amp;nbsp;The regular text path peaked at 18.93 seconds p90 for the requirement turn, while agent creation returned its first message in 8.68 seconds on average but needed 163.71 seconds on average, and 202.37 seconds at p90, to complete.&lt;/P&gt;
&lt;H3&gt;File conversation latency&lt;/H3&gt;
&lt;img&gt;Figure 8. File intake: 155 conversations, 1,705 recorded samples, eight virtual users, and zero errors. Each section uses its own scale, so bar lengths compare within a section, not across sections.&lt;/img&gt;
&lt;P&gt;&lt;STRONG&gt;Observation.&lt;/STRONG&gt;&amp;nbsp;Attachment processing was the slowest file turn at 22.19 seconds average and 27.80 seconds p90; token, conversation-start, send, and upload calls remained at or below 220 milliseconds p90.&lt;/P&gt;
&lt;H2&gt;What the numbers suggest&lt;/H2&gt;
&lt;H3&gt;Agent work dominated the measured latency&lt;/H3&gt;
&lt;P&gt;During the Demo Run, Direct Line token, conversation-start, send, and upload operations all stayed below 220 milliseconds p90. Complete chat turns took seconds, while the agent-creation branch took minutes. This shows that most of the measured end-to-end time accumulated after Direct Line accepted the message. The results do not separate orchestration, model, tool, or downstream-service time.&lt;/P&gt;
&lt;H3&gt;TTFB did not describe the complete answer&lt;/H3&gt;
&lt;P&gt;Some turns returned one message, so TTFB and ResponseTime were equal. Others acknowledged the request and kept working. The target turn averaged 3.08 seconds to first reply and 6.62 seconds to completion. The conditional action turn averaged 3.29 seconds to first reply and 9.46 seconds to completion. Agent creation widened that gap to more than two and a half minutes on average. Measuring only the first reply would hide the expensive part of those turns.&lt;/P&gt;
&lt;H3&gt;Equal users did not produce equal conversation totals&lt;/H3&gt;
&lt;P&gt;The user allocation was eight and eight, but the file path completed 155 conversations while the text path completed 98. That difference is consistent with the longer multi-turn text flow and its occasional agent-creation branch. Virtual-user allocation describes concurrency; completed iterations also depend on scenario duration.&lt;/P&gt;
&lt;H3&gt;Assumptions and guardrails&lt;/H3&gt;
&lt;P&gt;A few deliberate choices bound what these numbers mean:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Baseline scale, on purpose.&lt;/STRONG&gt;&amp;nbsp;The four-user local validation and 16-user Azure profile generate baselines, not a stress test. The guidance warns that load exceeding real user behavior can trigger message-consumption overage and environment throttling, so the Demo Run stayed within confirmed traffic and quota boundaries.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;WebSocket transport with secret auth.&lt;/STRONG&gt;&amp;nbsp;The client uses Direct Line over WebSockets to match the tested client application and exchanges a Direct Line secret for a token. A test for a client that receives activities through HTTP GET should reproduce that transport instead.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;One agent, one region.&lt;/STRONG&gt;&amp;nbsp;The numbers describe a single agent on one Direct Line regional endpoint; a different agent, model, or region will have its own signature.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;New-experience behavior observed in this run.&lt;/STRONG&gt;&amp;nbsp;The client relies on the&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;turn.complete&lt;/SPAN&gt;&amp;nbsp;event and role-optional replies observed during the test. Both the product status and response shapes can change while the experience remains in preview.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Capacity confirmed first.&lt;/STRONG&gt; A larger run requires prior confirmation that the agent, environment, and connected services support the peak throughput, with a limit increase requested when estimates exceed defaults.&lt;/LI&gt;
&lt;/UL&gt;
&lt;H3&gt;Limitations&lt;/H3&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Single Azure engine, light load.&lt;/STRONG&gt;&amp;nbsp;Sixteen virtual users provide a baseline, not a capacity ceiling. Characterizing saturation needs higher concurrency and multiple Azure Load Testing engines.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;One Demo Run.&lt;/STRONG&gt;&amp;nbsp;The results describe this 30-minute window and should be compared with repeated runs before setting a service-level target.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Chat turns only.&lt;/STRONG&gt;&amp;nbsp;The harness measures the message turn. It doesn't exercise sign-in cards, adaptive-card submits, or streamed token-by-token rendering.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;No Azure transcripts.&lt;/STRONG&gt;&amp;nbsp;The cloud profile intentionally disabled readable transcripts and client JSONL, so the five agent-creation turns prove completed responses but not an independent resource inventory.&lt;/LI&gt;
&lt;/UL&gt;
&lt;H3&gt;One operational note&lt;/H3&gt;
&lt;P&gt;&lt;STRONG&gt;Operational note.&lt;/STRONG&gt;&amp;nbsp;Confirm the Copilot Studio environment's quotas and the capacity of every connected dependency before increasing users or engine count. The workload should model expected traffic rather than use production systems as an unrestricted stress target.&lt;/P&gt;
&lt;H2&gt;Wrap-up&lt;/H2&gt;
&lt;P&gt;For the tested Copilot Studio new-experience agent and WebSocket client, a small reusable Locust client captured the observed Direct Line flow. It matched the client's WebSocket transport, treated non-echo messages as replies, stopped on the&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;t&lt;/SPAN&gt;&lt;SPAN class="lia-text-color-12"&gt;&lt;SPAN class="lia-text-color-12"&gt;u&lt;/SPAN&gt;rn.complete&lt;/SPAN&gt;&amp;nbsp;event emitted by the tested product build, and recorded both first-reply and last-reply times so a multi-message turn did not hide behind its acknowledgment.&lt;/P&gt;
&lt;P&gt;The same Python workload runs unchanged from a laptop and from Azure Load Testing engines. Local settings come from&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;locust.conf&lt;/SPAN&gt;; Azure settings come from&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;locust.azure.conf&amp;nbsp;&lt;/SPAN&gt;and the test YAML. The Direct Line secret is supplied at run time, and a&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;failureCriteria&amp;nbsp;&lt;/SPAN&gt;gate can support a release pipeline. The four-user local run validated the scripts and diagnostics; the Demo Run then held its full steady concurrency with 3,212 samples and zero failures. Further tests can add repeated baselines, more users and engines for saturation, additional attachment types, and a longer soak.&lt;/P&gt;
&lt;H3&gt;Learn more&lt;/H3&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;A href="https://learn.microsoft.com/en-us/microsoft-copilot-studio/guidance/conversational-agents-performance-testing" target="_blank" rel="noopener"&gt;Plan and create a conversational agent performance test&lt;/A&gt;&amp;nbsp;— the planning method, workload model, and test-plan structure this post follows.&lt;/LI&gt;
&lt;LI&gt;&lt;A href="https://learn.microsoft.com/en-us/microsoft-copilot-studio/guidance/conversational-agents-performance-improvement" target="_blank" rel="noopener"&gt;Best practices for improving conversational agent performance&lt;/A&gt;&amp;nbsp;— quotas, and the agent-side levers for cutting latency.&lt;/LI&gt;
&lt;LI&gt;&lt;A href="https://learn.microsoft.com/en-us/microsoft-copilot-studio/agents-experience/overview" target="_blank" rel="noopener"&gt;Agents overview (new experience)&lt;/A&gt;&amp;nbsp;— the orchestration model, instructions, knowledge, tools, and connected agents.&lt;/LI&gt;
&lt;LI&gt;&lt;A href="https://docs.locust.io/" target="_blank" rel="noopener"&gt;Locust documentation&lt;/A&gt;&amp;nbsp;— the load-testing framework this harness builds on.&lt;/LI&gt;
&lt;LI&gt;&lt;A href="https://learn.microsoft.com/azure/load-testing/" target="_blank" rel="noopener"&gt;Azure Load Testing documentation&lt;/A&gt; — the managed load-testing service.&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2&gt;Get the code&lt;/H2&gt;
&lt;P&gt;The complete runnable example is available in&amp;nbsp;&lt;A href="https://github.com/kroy92/copilot-studio-load-testing" target="_blank" rel="noopener"&gt;kroy92/copilot-studio-load-testing&lt;/A&gt;. The repository contains&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;new_chat_client.py&lt;/SPAN&gt;, the two-class&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;locustfile_discovery_demo.py&lt;/SPAN&gt;&amp;nbsp;workload, three attachment fixtures, local and Azure Locust configuration files,&lt;SPAN class="lia-text-color-12"&gt;&amp;nbsp;requirements.txt&lt;/SPAN&gt;, and&amp;nbsp;&lt;SPAN class="lia-text-color-12"&gt;azure-loadtest.yaml&lt;/SPAN&gt;.&lt;/P&gt;
&lt;P&gt;The Direct Line secret stays in the ignored&lt;SPAN class="lia-text-color-12"&gt; .env&lt;/SPAN&gt; file locally. For new Azure Load Testing runs, the recommended command retrieves it from Azure Key Vault through the dedicated secret parameter. The same Python workload runs in both environments, with &lt;SPAN class="lia-text-color-12"&gt;locust.conf&lt;/SPAN&gt; used locally and &lt;SPAN class="lia-text-color-12"&gt;locust.azure.conf&lt;/SPAN&gt; used in Azure.&lt;/P&gt;</description>
      <pubDate>Wed, 15 Jul 2026 09:47:26 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/load-testing-copilot-studio-agents-with-locust-and-azure-load/ba-p/4535445</guid>
      <dc:creator>Krishna-Roy</dc:creator>
      <dc:date>2026-07-15T09:47:26Z</dc:date>
    </item>
    <item>
      <title>Orchestrate Azure Container Apps Jobs with Apache Airflow</title>
      <link>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/orchestrate-azure-container-apps-jobs-with-apache-airflow/ba-p/4534607</link>
      <description>&lt;P&gt;Azure Container Apps (ACA) Jobs are a great way to run work that starts, does something, and finishes: nightly batch, data processing, ETL, ML scoring, report generation. They scale to zero, bill per execution, and run any container you give them.&lt;/P&gt;
&lt;P&gt;But the moment your "one job" becomes "a set of jobs that depend on each other," a gap appears:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;EM&gt;How do I run twenty jobs in parallel, wait for all of them, then run one more job only if they all succeeded — and retry just the one that failed?&lt;/EM&gt;&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;P&gt;A single ACA Job can't express that on its own. What you're describing is an &lt;STRONG&gt;orchestrator&lt;/STRONG&gt;, and the most widely adopted one in the data world is &lt;A href="https://airflow.apache.org/" target="_blank" rel="noopener"&gt;Apache Airflow&lt;/A&gt;.&lt;/P&gt;
&lt;P&gt;This post introduces two open-source templates that connect the two, so Airflow becomes the brain and ACA Jobs become the muscle. Pick the one that matches what you already run:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;A href="https://github.com/hetvip2/airflow-on-aca-jobs" target="_blank" rel="noopener"&gt;airflow-on-aca-jobs&lt;/A&gt;&lt;/STRONG&gt;: you already have Airflow. Drop in an operator and point it at ACA Jobs. &lt;STRONG&gt;Host nothing new.&lt;/STRONG&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;A href="https://github.com/hetvip2/airflow-hosted-on-aca" target="_blank" rel="noopener"&gt;airflow-hosted-on-aca&lt;/A&gt;&lt;/STRONG&gt;: you don't have Airflow. Get a full one running on Azure Container Apps with &lt;STRONG&gt;one command.&lt;/STRONG&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Both use the &lt;STRONG&gt;same operator&lt;/STRONG&gt; and the &lt;STRONG&gt;same DAGs&lt;/STRONG&gt;, so you can start with one and move to the other later without rewriting your workflows.&lt;/P&gt;
&lt;div data-video-id="https://www.youtube.com/watch?v=yzax4oihm9Q/1784065110574" data-video-remote-vid="https://www.youtube.com/watch?v=yzax4oihm9Q/1784065110574" class="lia-video-container lia-media-is-center lia-media-size-large"&gt;&lt;iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2Fyzax4oihm9Q%3Ffeature%3Doembed&amp;amp;display_name=YouTube&amp;amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Dyzax4oihm9Q&amp;amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2Fyzax4oihm9Q%2Fhqdefault.jpg&amp;amp;type=text%2Fhtml&amp;amp;schema=youtube" allowfullscreen="" style="max-width: 100%"&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;!-- INSERT DEMO VIDEO HERE --&gt;
&lt;P&gt;&lt;EM&gt;See Airflow orchestrate real ACA Job executions with parallel fan-out, dependency ordering, and automatic retries.&lt;/EM&gt;&lt;/P&gt;
&lt;!-- INSERT HERO IMAGE: blog-hero.png — Airflow DAG -&gt; AzureContainerAppsJobOperator -&gt; ACA Jobs flow, with the two adoption paths (bring your own Airflow / Airflow hosted on ACA) at the bottom. Alt text: "Airflow orchestrates Azure Container Apps Jobs: an Airflow DAG hands each task to the AzureContainerAppsJobOperator, which starts one ACA Job execution per unit of work and waits for the result before continuing the graph." --&gt;
&lt;H2&gt;Why ACA Jobs need an orchestrator&lt;/H2&gt;
&lt;P&gt;A plain ACA Job is great at one thing: run this container to completion, then stop. That covers a scheduled job or a one-off task perfectly.&lt;/P&gt;
&lt;P&gt;Real pipelines need more than that:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Dependency ordering:&lt;/STRONG&gt; step B runs only after step A succeeds.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Parallel fan-out:&lt;/STRONG&gt; launch one execution per file, per store, or per partition, all at once, then wait for the whole batch.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Per-task retries:&lt;/STRONG&gt; if one execution in a batch of fifty fails, retry just that one, not the other forty-nine.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Backfills and scheduling:&lt;/STRONG&gt; re-run yesterday's pipeline, or run every night with a full history of what happened.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;These are the problems an orchestrator solves. Instead of building that logic yourself, you let Airflow handle the graph, the scheduling, and the retries, while ACA Jobs run the compute. You get serverless, scale-to-zero workers, and you didn't have to stand up a scheduler to get them.&lt;/P&gt;
&lt;H2&gt;The operator that ties them together&lt;/H2&gt;
&lt;P&gt;Both templates ship the same small plugin: an Airflow operator called &lt;CODE&gt;AzureContainerAppsJobOperator&lt;/CODE&gt;. In a DAG it looks like any other task:&lt;/P&gt;
&lt;PRE&gt;&lt;CODE&gt;report_sales = AzureContainerAppsJobOperator(
    task_id="report_store_sales",
    subscription_id="{{ var.value.azure_subscription_id }}",
    resource_group="{{ var.value.aca_resource_group }}",
    job_name="{{ var.value.aca_job_name }}",
    image="python:3.12-slim",
    command=["python", "-c", MY_PROGRAM],
    env_vars={"STORE_NAME": "Seattle"},
    deferrable=True,
)&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;A few things make this operator easy to work with:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Per-execution overrides.&lt;/STRONG&gt; It takes the ACA Job you point it at and overrides the &lt;CODE&gt;image&lt;/CODE&gt;, &lt;CODE&gt;command&lt;/CODE&gt;, &lt;CODE&gt;args&lt;/CODE&gt;, and &lt;CODE&gt;env_vars&lt;/CODE&gt; for that run. You can drive many different workloads from a single ACA Job definition, and you don't need to build or push a custom image just to try something. The example above runs the stock &lt;CODE&gt;python:3.12-slim&lt;/CODE&gt; image with an inline program.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Deferrable by default.&lt;/STRONG&gt; With &lt;CODE&gt;deferrable=True&lt;/CODE&gt;, Airflow frees its worker slot while the ACA Job runs and resumes when it finishes. That means your fan-out width is bounded by ACA, not by how many Airflow workers you have. You can launch dozens of parallel executions cheaply.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;No secrets required.&lt;/STRONG&gt; Authentication resolves in a sensible order: an Airflow Connection if you set one, otherwise an &lt;CODE&gt;AZURE_ACCESS_TOKEN&lt;/CODE&gt; environment variable, otherwise &lt;CODE&gt;DefaultAzureCredential&lt;/CODE&gt; (managed identity). In Azure, the hosted template uses a managed identity so nothing sensitive is stored in Airflow at all.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Because both templates share this operator, a DAG written for one runs unchanged on the other.&lt;/P&gt;
&lt;H2&gt;Option 1: Bring your own Airflow (host nothing)&lt;/H2&gt;
&lt;P&gt;Choose &lt;STRONG&gt;&lt;A href="https://github.com/hetvip2/airflow-on-aca-jobs" target="_blank" rel="noopener"&gt;airflow-on-aca-jobs&lt;/A&gt;&lt;/STRONG&gt; if you already run Airflow: Azure Managed Airflow, MWAA, Astronomer, or your own deployment. You keep that Airflow exactly as it is and simply teach it to talk to ACA Jobs.&lt;/P&gt;
&lt;PRE&gt;&lt;CODE&gt;+------------------------------------------+
|  Your Airflow  (you host it, unchanged)  |
|  runs AzureContainerAppsJobOperator      |
+------------------------------------------+
                     |
                     |  ACA Jobs REST API
                     v
+------------------------------------------+
|  ACA Job  (Azure Container Apps)         |
|                                          |
|  store 1 | store 2 | ... | store N       |
|  parallel executions -&amp;gt; scale to zero    |
+------------------------------------------+
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;&lt;EM&gt;Your existing Airflow runs the operator; ACA Jobs run the work. You host nothing new.&lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;Adoption is three small steps:&lt;/P&gt;
&lt;OL&gt;
&lt;LI&gt;&lt;STRONG&gt;Copy the operator&lt;/STRONG&gt; into your Airflow's &lt;CODE&gt;plugins/&lt;/CODE&gt; folder.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Add a DAG&lt;/STRONG&gt; that uses &lt;CODE&gt;AzureContainerAppsJobOperator&lt;/CODE&gt;.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Set three Airflow Variables&lt;/STRONG&gt; so the operator knows which job to drive:&lt;/LI&gt;
&lt;/OL&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;&lt;table border="1" style="border-width: 1px;"&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;STRONG&gt;Airflow Variable&lt;/STRONG&gt;&lt;/th&gt;&lt;th&gt;&lt;STRONG&gt;Value&lt;/STRONG&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;CODE&gt;azure_subscription_id&lt;/CODE&gt;&lt;/td&gt;&lt;td&gt;your subscription id&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;CODE&gt;aca_resource_group&lt;/CODE&gt;&lt;/td&gt;&lt;td&gt;the resource group holding the ACA Job&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;CODE&gt;aca_job_name&lt;/CODE&gt;&lt;/td&gt;&lt;td&gt;the ACA Job name&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;colgroup&gt;&lt;col style="width: 50.00%" /&gt;&lt;col style="width: 50.00%" /&gt;&lt;/colgroup&gt;&lt;/table&gt;&lt;/DIV&gt;
&lt;P&gt;That's the whole integration. Nothing new to host, no extra scheduler or database, no custom image. ACA Jobs just become another task type Airflow can call.&lt;/P&gt;
&lt;P&gt;If you want a job to point at first, the template includes an &lt;A href="https://learn.microsoft.com/azure/developer/azure-developer-cli/" target="_blank" rel="noopener"&gt;Azure Developer CLI&lt;/A&gt; (&lt;CODE&gt;azd&lt;/CODE&gt;) deployment that stands up a sample ACA Job for you:&lt;/P&gt;
&lt;PRE&gt;&lt;CODE&gt;git clone https://github.com/hetvip2/airflow-on-aca-jobs
cd airflow-on-aca-jobs
azd up          # deploys a sample ACA Job, prints its resource group + name&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;Then copy &lt;CODE&gt;airflow/plugins/&lt;/CODE&gt; and &lt;CODE&gt;airflow/dags/&lt;/CODE&gt; into your Airflow, set the three Variables, and trigger the DAG.&lt;/P&gt;
&lt;H2&gt;Option 2: Airflow hosted on ACA (turnkey)&lt;/H2&gt;
&lt;P&gt;Choose &lt;STRONG&gt;&lt;A href="https://github.com/hetvip2/airflow-hosted-on-aca" target="_blank" rel="noopener"&gt;airflow-hosted-on-aca&lt;/A&gt;&lt;/STRONG&gt; if you don't already have an orchestrator and want one running next to your jobs. One command provisions the whole thing on Azure Container Apps:&lt;/P&gt;
&lt;PRE&gt;&lt;CODE&gt;                   azd up
                     |
                     v
+------------------------------------------+
|  Airflow control plane on ACA            |
|  web  |  scheduler  |  triggerer         |
|  Postgres (metadata) + Azure Files (dags)|
|  Managed Identity - no secrets stored    |
+------------------------------------------+
                     |
                     |  ACA Jobs REST API
                     v
+------------------------------------------+
|  ACA Job  (Azure Container Apps)         |
|                                          |
|  store 1 | store 2 | ... | store N       |
|  parallel executions -&amp;gt; scale to zero    |
+------------------------------------------+
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;&lt;EM&gt;One command deploys the whole Airflow control plane on ACA, right next to the jobs it drives.&lt;/EM&gt;&lt;/P&gt;
&lt;PRE&gt;&lt;CODE&gt;git clone https://github.com/hetvip2/airflow-hosted-on-aca
cd airflow-hosted-on-aca
azd env new my-airflow
azd up          # prints your Airflow URL when it finishes&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;&lt;CODE&gt;azd up&lt;/CODE&gt; deploys a complete, working Airflow control plane on ACA:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;airflow-web&lt;/STRONG&gt;, &lt;STRONG&gt;airflow-scheduler&lt;/STRONG&gt;, and &lt;STRONG&gt;airflow-triggerer&lt;/STRONG&gt; running as Container Apps on LocalExecutor, so there's no Celery or Redis to operate.&lt;/LI&gt;
&lt;LI&gt;A &lt;STRONG&gt;Postgres&lt;/STRONG&gt; metadata database.&lt;/LI&gt;
&lt;LI&gt;A &lt;STRONG&gt;user-assigned managed identity&lt;/STRONG&gt; with permission to call the ACA Jobs API, so the operator authenticates with no secrets stored in Airflow.&lt;/LI&gt;
&lt;LI&gt;A &lt;STRONG&gt;sample ACA Job&lt;/STRONG&gt; for Airflow to drive out of the box.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Your DAGs and plugins live on a mounted &lt;STRONG&gt;Azure Files&lt;/STRONG&gt; share, so you ship new workflows by re-uploading files rather than rebuilding an image:&lt;/P&gt;
&lt;PRE&gt;&lt;CODE&gt;cp my_dag.py airflow/dags/
azd hooks run postprovision   # uploads dags + plugins to the share&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;Airflow picks up the change within a minute. You now own a real orchestrator, hosted serverlessly on the same platform as your jobs.&lt;/P&gt;
&lt;H2&gt;Which one should you pick?&lt;/H2&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;&lt;table border="1" style="border-width: 1px;"&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&amp;nbsp;&lt;/th&gt;&lt;th&gt;&lt;STRONG&gt;Option 1: &lt;CODE&gt;airflow-on-aca-jobs&lt;/CODE&gt;&lt;/STRONG&gt;&lt;/th&gt;&lt;th&gt;&lt;STRONG&gt;Option 2: &lt;CODE&gt;airflow-hosted-on-aca&lt;/CODE&gt;&lt;/STRONG&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;STRONG&gt;Best when&lt;/STRONG&gt;&lt;/td&gt;&lt;td&gt;You already run Airflow&lt;/td&gt;&lt;td&gt;You don't have Airflow yet&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;STRONG&gt;Setup&lt;/STRONG&gt;&lt;/td&gt;&lt;td&gt;Copy the operator + a DAG + 3 Variables&lt;/td&gt;&lt;td&gt;&lt;CODE&gt;azd up&lt;/CODE&gt; (one command)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;STRONG&gt;Who hosts Airflow&lt;/STRONG&gt;&lt;/td&gt;&lt;td&gt;You do (unchanged)&lt;/td&gt;&lt;td&gt;Azure Container Apps&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;STRONG&gt;Authentication&lt;/STRONG&gt;&lt;/td&gt;&lt;td&gt;Connection or short-lived token&lt;/td&gt;&lt;td&gt;Managed identity, nothing stored&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;STRONG&gt;Ownership&lt;/STRONG&gt;&lt;/td&gt;&lt;td&gt;Lowest: nothing new to run&lt;/td&gt;&lt;td&gt;Turnkey: a full orchestrator you own&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;colgroup&gt;&lt;col style="width: 33.33%" /&gt;&lt;col style="width: 33.33%" /&gt;&lt;col style="width: 33.33%" /&gt;&lt;/colgroup&gt;&lt;/table&gt;&lt;/DIV&gt;
&lt;P&gt;The important part: &lt;STRONG&gt;the workload never changes.&lt;/STRONG&gt; The same DAG and the same operator drive the same ACA Job executions in both. Start wherever you are today, and switch later with zero changes to your pipelines.&lt;/P&gt;
&lt;H2&gt;See it end to end&lt;/H2&gt;
&lt;P&gt;Picture a retailer that wants one number every night: total sales across all stores. Each store reports its own sales as a separate ACA Job execution, all running in parallel. When every store is in, a final job adds them into the company total.&lt;/P&gt;
&lt;P&gt;That one workflow exercises exactly what a plain Job can't do alone:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;parallel fan-out:&lt;/STRONG&gt; one ACA Job execution per store, all at once&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;dependency ordering:&lt;/STRONG&gt; the roll-up runs only after every store reports&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;per-task retries:&lt;/STRONG&gt; if a store's execution fails, Airflow retries just that store, and the nightly total still lands&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;In Airflow's Graph view you watch the store tasks light up together, then the roll-up run last. In the Azure portal you watch real executions appear under your ACA Job and scale back to zero when they finish. Same job, same DAG, whichever template you chose.&lt;/P&gt;
&lt;H2&gt;Call to action&lt;/H2&gt;
&lt;P&gt;If you run batch, ETL, or any multi-step work on Azure Container Apps Jobs, give one of these templates a try:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Already have Airflow? Start with &lt;STRONG&gt;&lt;A href="https://github.com/hetvip2/airflow-on-aca-jobs" target="_blank" rel="noopener"&gt;airflow-on-aca-jobs&lt;/A&gt;&lt;/STRONG&gt;.&lt;/LI&gt;
&lt;LI&gt;Need an orchestrator? Start with &lt;STRONG&gt;&lt;A href="https://github.com/hetvip2/airflow-hosted-on-aca" target="_blank" rel="noopener"&gt;airflow-hosted-on-aca&lt;/A&gt;&lt;/STRONG&gt;.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Both are open source, deploy with &lt;CODE&gt;azd up&lt;/CODE&gt;, and share the same operator so you can move between them freely. Try them out and let us know what you orchestrate.&lt;/P&gt;</description>
      <pubDate>Tue, 14 Jul 2026 21:41:26 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/orchestrate-azure-container-apps-jobs-with-apache-airflow/ba-p/4534607</guid>
      <dc:creator>hetvip</dc:creator>
      <dc:date>2026-07-14T21:41:26Z</dc:date>
    </item>
    <item>
      <title>From AI Adoption to AI Governance - Using APIM as the Gateway for Azure AI Foundry</title>
      <link>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/from-ai-adoption-to-ai-governance-using-apim-as-the-gateway-for/ba-p/4536247</link>
      <description>&lt;P&gt;&lt;EM&gt;Co-authored by Gaurav Jain (Senior Cloud Solution Architect @ Microsoft) and Abhishek Mittal (Cloud Solution Architect @ Microsoft)&lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;Enterprises move through three phases of AI adoption:&amp;nbsp;&lt;STRONG&gt;evaluating&lt;/STRONG&gt; models, &lt;STRONG&gt;building&lt;/STRONG&gt; apps and agents, and &lt;STRONG&gt;operationalizing&lt;/STRONG&gt; them in production. The first two are easier to accelerate. The third is where governance becomes critical. Once multiple teams share an AI endpoint, leaders need clear answers to practical questions: which model consumed tokens, which team used them, and who is authorized to call it?&lt;/P&gt;
&lt;P&gt;This post shows how to place &lt;STRONG&gt;Azure API Management (APIM) in front of Azure AI Foundry as an AI Gateway&lt;/STRONG&gt;, turning a shared endpoint into a governed control point for per-model token visibility, chargeback, and budget alerts — with &lt;STRONG&gt;no changes to client code&lt;/STRONG&gt;. It also shows where Azure Front Door and Web Application Firewall (WAF) fit in a secure AI Landing Zone.&lt;/P&gt;
&lt;H1&gt;The problem: AI adoption is outpacing AI governance&lt;/H1&gt;
&lt;P&gt;A common starting point is an Azure OpenAI resource running multiple models. The team already has operational telemetry, but governance needs a different view: per-model token usage for chargeback, budget alerts, and capacity planning, captured in one place.&lt;/P&gt;
&lt;P&gt;Azure gives you rich resource-level telemetry out of the box, and that is exactly where we started:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Azure Monitor — Metrics blade: &lt;/STRONG&gt;shows token usage split by model and deployment in near real time.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Diagnostic settings: &lt;/STRONG&gt;stream the resource's metrics and request logs into Log Analytics (the AzureMetrics and AzureDiagnostics tables).&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Azure Monitor provides useful resource-level telemetry, including metrics and diagnostic logs. A governance view needs something different: model identity and token counts correlated in a single record, so teams can build a per-model, month-to-date ledger for chargeback and alerting.&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;The AzureMetrics table carries the token totals, aggregated at the resource level.&lt;/LI&gt;
&lt;LI&gt;The AzureDiagnostics logs carry the model and deployment name at the request level.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Each stream does its job well. Correlating them into one per-model, month-to-date ledger — and alerting on it — is a governance concern that sits &lt;EM&gt;above&lt;/EM&gt; any single resource. Azure Monitor metric alerts, for instance, work on a rolling 24-hour window that maps cleanly onto a per-&lt;EM&gt;day&lt;/EM&gt; token budget; a month-to-date, per-model chargeback ledger is simply a different shape of question — and a natural fit for a dedicated control point.&lt;/P&gt;
&lt;P&gt;This transition is the focus of this post: moving from AI &lt;STRONG&gt;adoption&lt;/STRONG&gt; to AI &lt;STRONG&gt;governance&lt;/STRONG&gt; by introducing a control point where model identity and token usage are captured together by design. The natural home for that control point is an AI gateway — and we build it next with Azure API Management in front of Azure AI Foundry.&lt;/P&gt;
&lt;H1&gt;The pattern: APIM as the AI Gateway for Azure AI Foundry&lt;/H1&gt;
&lt;P&gt;The AI gateway in Azure API Management is a set of capabilities to secure, scale, monitor, and govern the AI models, agents, and tools behind your apps. It isn't a separate product — it extends the existing API Management gateway. As Microsoft's guidance puts it, as AI adoption matures the gateway helps you authenticate and authorize access to AI services, load balance across endpoints, monitor and log AI interactions, and manage token usage and quotas across multiple applications.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;APIM becomes the &lt;/STRONG&gt;governed front door &lt;STRONG&gt;for &lt;/STRONG&gt;Azure AI Foundry. Clients continue calling an OpenAI-compatible endpoint; APIM authenticates to Foundry with a &lt;STRONG&gt;system-assigned managed identity&lt;/STRONG&gt;, forwards the request, and emits per-model token telemetry to Azure Monitor and Application Insights. The result is per-model visibility without client-side changes.&lt;/P&gt;
&lt;H4&gt;Models behind the gateway&lt;/H4&gt;
&lt;P&gt;The gateway can front &lt;STRONG&gt;any&lt;/STRONG&gt; model deployment in Azure AI Foundry — Azure OpenAI models, other Foundry models, or a mix — and the pattern is identical no matter which you run. For a concrete reference, the walkthrough in this post sits in front of two existing deployments:&lt;/P&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;&lt;table border="1" style="border-width: 1px;"&gt;&lt;thead&gt;&lt;tr&gt;&lt;td&gt;
&lt;P&gt;&lt;STRONG&gt;Deployment&lt;/STRONG&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;STRONG&gt;Model&lt;/STRONG&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;STRONG&gt;Provisioned capacity (TPM)&lt;/STRONG&gt;&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;
&lt;P&gt;gpt-4.1&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;gpt-4.1&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;500K&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
&lt;P&gt;gpt-5&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;gpt-5&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;50K&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;colgroup&gt;&lt;col style="width: 33.33%" /&gt;&lt;col style="width: 33.33%" /&gt;&lt;col style="width: 33.33%" /&gt;&lt;/colgroup&gt;&lt;/table&gt;&lt;/DIV&gt;
&lt;P&gt;&lt;EM&gt;Example deployments referenced throughout this post. Note the deliberate capacity gap — gpt-5 at 50K TPM versus 500K for gpt-4.1 — exactly the kind of asymmetry that makes per-model visibility a governance requirement, not a nice-to-have.&lt;/EM&gt;&lt;/P&gt;
&lt;H4&gt;Architecture&lt;/H4&gt;
&lt;img /&gt;
&lt;P&gt;&lt;EM&gt;Figure 1 — Architecture / component flow: consumers call one governed API; the inbound policy authenticates with a managed identity, resolves the model, and emits per-model token metrics to Azure Monitor and Application Insights.&lt;/EM&gt;&lt;/P&gt;
&lt;H1&gt;The starting point (“before”): a pass-through without a usage signal&lt;/H1&gt;
&lt;P&gt;By default, APIM operates as a straightforward proxy. If you import a Foundry API and keep the default configuration, the policy simply selects the backend service:&lt;/P&gt;
&lt;LI-CODE lang="html"&gt;&amp;lt;policies&amp;gt;
  &amp;lt;inbound&amp;gt;
    &amp;lt;base /&amp;gt;
    &amp;lt;set-backend-service id="apim-generated-policy" backend-id="foundry-backend" /&amp;gt;
  &amp;lt;/inbound&amp;gt;
  &amp;lt;backend&amp;gt;&amp;lt;base /&amp;gt;&amp;lt;/backend&amp;gt;
  &amp;lt;outbound&amp;gt;&amp;lt;base /&amp;gt;&amp;lt;/outbound&amp;gt;
  &amp;lt;on-error&amp;gt;&amp;lt;base /&amp;gt;&amp;lt;/on-error&amp;gt;
&amp;lt;/policies&amp;gt;&lt;/LI-CODE&gt;
&lt;P&gt;&lt;EM&gt;A plain pass-through API. It forwards traffic faithfully — it simply doesn't surface a usage signal yet. This is our “before.”&lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;Pass-through configuration works, but it does not distinguish traffic by model. All requests flow through the same stream, with no per-model chargeback signal, no capacity warning, and no clear view of which deployment is driving consumption. To govern the workload, the gateway must &lt;EM&gt;understand&lt;/EM&gt; the traffic — not just relay it.&lt;/P&gt;
&lt;H1&gt;The governed gateway (“after”): a policy that sees every token&lt;/H1&gt;
&lt;P&gt;The custom inbound policy below is the heart of the pattern. It does four things in order:&lt;/P&gt;
&lt;OL&gt;
&lt;LI&gt;set-backend-service — select the Azure AI Foundry backend.&lt;/LI&gt;
&lt;LI&gt;authentication-managed-identity — obtain an Entra ID token for cognitiveservices.azure.com using the APIM system-assigned identity. No keys ever leave the gateway.&lt;/LI&gt;
&lt;LI&gt;set-variable deployment-id — resolve the model name from either the URL path or the request body (more on why below).&lt;/LI&gt;
&lt;LI&gt;azure-openai-emit-token-metric — emit prompt, completion, and total token counts to Azure Monitor, dimensioned by model.&lt;/LI&gt;
&lt;/OL&gt;
&lt;LI-CODE lang="html"&gt;&amp;lt;policies&amp;gt;
  &amp;lt;inbound&amp;gt;
    &amp;lt;base /&amp;gt;
    &amp;lt;set-backend-service backend-id="foundry-backend" /&amp;gt;
    &amp;lt;authentication-managed-identity resource="https://cognitiveservices.azure.com" /&amp;gt;

    &amp;lt;!-- Resolve the model/deployment name. The Foundry Model Inference API
         (/models/chat/completions, /models/embeddings, /anthropic/v1/messages)
         passes it in the JSON body as "model". The Azure OpenAI-style surface
         (/openai/deployments/{name}/...) passes it in the URL path. Handle both. --&amp;gt;

    &amp;lt;set-variable name="deployment-id" value="@{
        var path = context.Request.Url.Path ?? "";
        var m = System.Text.RegularExpressions.Regex.Match(path, "/deployments/([^/?]+)");
        if (m.Success) { return m.Groups[1].Value; }
        try {
            var body = context.Request.Body?.As&amp;lt;JObject&amp;gt;(preserveContent: true);
            var model = body?["model"];
            if (model != null &amp;amp;&amp;amp; !string.IsNullOrEmpty(model.ToString())) {
                return model.ToString();
            }
        } catch (Exception) { }
        return "unknown";
    }" /&amp;gt;

    &amp;lt;!-- Emit token-usage metrics dimensioned by model, so consumption can be
         sliced per model in Azure Monitor / Application Insights. --&amp;gt;

    &amp;lt;azure-openai-emit-token-metric namespace="genai-tokens"&amp;gt;
        &amp;lt;dimension name="ModelDeploymentName" value="@((string)context.Variables["deployment-id"])" /&amp;gt;
        &amp;lt;dimension name="ModelName" value="@((string)context.Variables["deployment-id"])" /&amp;gt;
        &amp;lt;dimension name="APIId" value="@(context.Api.Id)" /&amp;gt;
        &amp;lt;dimension name="Subscription" value="@(context.Subscription?.Id ?? "none")" /&amp;gt;
        &amp;lt;dimension name="Client IP" value="@(context.Request.IpAddress)" /&amp;gt;
        &amp;lt;dimension name="Product ID" value="@(context.Product?.Id ?? "none")" /&amp;gt;
    &amp;lt;/azure-openai-emit-token-metric&amp;gt;
  &amp;lt;/inbound&amp;gt;
  &amp;lt;backend&amp;gt;
    &amp;lt;base /&amp;gt;
  &amp;lt;/backend&amp;gt;
  &amp;lt;outbound&amp;gt;
    &amp;lt;base /&amp;gt;
  &amp;lt;/outbound&amp;gt;
  &amp;lt;on-error&amp;gt;
    &amp;lt;base /&amp;gt;
  &amp;lt;/on-error&amp;gt;
&amp;lt;/policies&amp;gt; 
&lt;/LI-CODE&gt;
&lt;P&gt;&lt;EM&gt;The full custom policy, applied at API scope. The highlighted value is the model-name resolution feeding a per-model token metric.&lt;/EM&gt;&lt;/P&gt;
&lt;H4&gt;Request flow, end to end&lt;/H4&gt;
&lt;P&gt;Figure 2 traces a single chat request from top to bottom — from the caller, through the gateway's inbound policy, out to Azure AI Foundry, and into your telemetry.&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&lt;EM&gt;Figure 2 — Per-request flow: authentication, model resolution, forwarding, and token-metric emission.&lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;The request flow is straightforward: the client calls the APIM endpoint, the gateway selects the Foundry backend, authenticates with managed identity, resolves the model name, forwards the request, emits token metrics, and returns the response unchanged. Governance is added at the gateway without requiring client-side changes.&lt;/P&gt;
&lt;H1&gt;Why dual-shape model resolution matters&lt;/H1&gt;
&lt;P&gt;The azure-openai-emit-token-metric policy can emit usage, but it still needs a model dimension. Different API surfaces place the model name in different locations: Foundry Model Inference and Anthropic-style requests use the body, while Azure OpenAI-compatible calls use the URL path. The policy handles both shapes, so one gateway can govern all callers consistently.&lt;/P&gt;
&lt;H1&gt;Observability: per-model token visibility and chargeback&lt;/H1&gt;
&lt;P&gt;Metrics land in Azure Monitor / Application Insights under the namespace genai-tokens. The policy records Total Tokens, Prompt Tokens, and Completion Tokens, each tagged with Model Name, Model Deployment Name, API Id, APIM Product Subscription, Client IP, and Product ID. The data can then be queried directly.&amp;nbsp;Per-model consumption over time:&lt;/P&gt;
&lt;LI-CODE lang="kusto"&gt;customMetrics
| where name == "Total Tokens"
| where timestamp &amp;gt;= startofmonth(now())
| extend ModelName = tostring(customDimensions["ModelName"])
| summarize TotalTokens = sum(valueSum), Calls = sum(valueCount)
    by ModelName
&lt;/LI-CODE&gt;
&lt;P&gt;&lt;EM&gt;Per-model token consumption (Application Insights customMetrics).&lt;/EM&gt;&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;And a per-model, per- API product subscription view for chargeback:&lt;/P&gt;
&lt;LI-CODE lang="kusto"&gt;customMetrics
| where name == "Total Tokens"
| where timestamp &amp;gt;= startofmonth(now())
| extend Model = tostring(customDimensions["ModelName"]),
         Sub   = tostring(customDimensions["Subscription"])
| summarize Tokens = sum(value) by Model, Sub, name
| order by Tokens desc&lt;/LI-CODE&gt;
&lt;P&gt;&lt;EM&gt;Chargeback: tokens by model and consuming subscription.&lt;/EM&gt;&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;The gateway records model identity and token usage together, so the chargeback view is built in. The same signal supports dashboards, daily budget alerts, capacity planning, and cost allocation by subscription or product — without changing client code.&lt;/P&gt;
&lt;H3&gt;Turning the signal into a Cost Guardrail: a 24-hour token alert&lt;/H3&gt;
&lt;P&gt;Because the token totals now carry the model name, you can put a hard guardrail on spend. Wrap a query in an Azure Monitor &lt;STRONG&gt;log search alert rule&lt;/STRONG&gt; that sums Total Tokens over the last 24 hours per model and returns only the deployments that breach a daily budget:&lt;/P&gt;
&lt;LI-CODE lang="kusto"&gt;// Rolling 24-hour token-budget guardrail — returns any model over its daily cap 

let dailyTokenBudget = 50000; // max Total Tokens per model in a rolling 24h window 
customMetrics | where name == "Total Tokens" 
| where timestamp &amp;gt; ago(24h) 
 extend Model = tostring(customDimensions["ModelName"])
 | summarize TokensLast24h = sum(value) by Model 
| where TokensLast24h &amp;gt; dailyTokenBudget 
| extend OverBudgetBy = TokensLast24h - dailyTokenBudget 
| project Model, TokensLast24h, DailyBudget = dailyTokenBudget, OverBudgetBy &lt;/LI-CODE&gt;
&lt;P&gt;&lt;EM&gt;A rolling 24-hour token-budget check. The alert rule fires whenever this query returns one or more rows.&lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;Configure this as a scheduled log search alert: evaluate on a short cadence (for example, hourly over the trailing 24-hour window), set the alert logic to fire when the &lt;STRONG&gt;result count is greater than zero&lt;/STRONG&gt;, and attach an action group that notifies the team through an email distribution list or Microsoft Teams channel. When any model crosses its rolling 24-hour token budget, the owning team is alerted, so overspend is detected within the day rather than at invoice time. Tune dailyTokenBudget per model, or add a single all-up cap, and translate token budgets into estimated daily cost ceilings to maintain continuous spend visibility.&lt;/P&gt;
&lt;H1&gt;Completing the picture: securing the AI Landing Zone with Front Door + WAF&lt;/H1&gt;
&lt;P&gt;&lt;STRONG&gt;APIM governs model usage; Azure Front Door with WAF governs public access.&lt;/STRONG&gt; Placing WAF at the edge protects the AI endpoint from common web attacks, malicious bots, abusive callers, and unwanted source IP ranges before traffic reaches APIM or Foundry.&lt;/P&gt;
&lt;H3&gt;What Front Door + WAF adds&lt;/H3&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;OWASP protection: &lt;/STRONG&gt;The Azure-managed Microsoft_DefaultRuleSet_2.1 helps defend against OWASP Top 10 web attacks and known CVEs; Microsoft_BotManagerRuleSet_1.0 helps block malicious bots. Run the policy in prevention mode so offending requests are rejected with a 403, not just logged.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;IP restriction and rate limiting:&amp;nbsp;&lt;/STRONG&gt;Custom WAF rules restrict access to known IP ranges and throttle abusive callers before they reach APIM or Foundry.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Global edge: &lt;/STRONG&gt;Front Door terminates TLS at the edge and provides a single, DDoS-protected public entry point for the workload.&lt;/LI&gt;
&lt;/UL&gt;
&lt;H3&gt;Defense in depth across the landing zone&lt;/H3&gt;
&lt;P&gt;Layered against the Azure AI Foundry landing zone baseline, the request path looks like this:&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&lt;EM&gt;Defense in depth: WAF at the edge, governance at the gateway, isolation on the network, Foundry reachable over a private endpoint.&lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;The baseline Azure AI Foundry landing zone reinforces every layer: private endpoints keep PaaS services (Foundry, Key Vault, Storage, AI Search) off the public internet; a system-assigned managed identity helps remove API keys; a hub-and-spoke topology routes egress through Azure Firewall; Azure Key Vault holds the Front Door TLS certificate; and Azure Policy enforces guardrails across the subscription. The gateway pattern from Sections 2–6 slots directly into this architecture as the governed control point for model traffic.&lt;/P&gt;
&lt;H1&gt;Outcomes and what to extend next&lt;/H1&gt;
&lt;P&gt;With APIM and the security edge in place, the shared endpoint supports per-model chargeback, capacity planning, zero client changes, and a stronger security baseline. The same gateway pattern can then be extended with token quotas, semantic caching, content safety, resiliency, and a unified model API (preview).&lt;/P&gt;
&lt;H1&gt;Closing thoughts&lt;/H1&gt;
&lt;P&gt;Moving from AI adoption to AI governance does not require re-architecting every app; it requires a &lt;STRONG&gt;control point&lt;/STRONG&gt;. APIM in front of Azure AI Foundry provides that point: one policy turns token usage into a per-model governance signal, and Front Door with WAF provides a hardened edge. Start with visibility, then add quotas, safety, and resiliency as adoption scales.&lt;/P&gt;
&lt;H2&gt;References&lt;/H2&gt;
&lt;OL&gt;
&lt;LI&gt;&lt;A href="https://learn.microsoft.com/en-us/azure/api-management/genai-gateway-capabilities" target="_blank" rel="noopener"&gt;AI gateway capabilities in Azure API Management — Microsoft Learn&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A href="https://learn.microsoft.com/en-us/azure/architecture/ai-ml/architecture/baseline-microsoft-foundry-landing-zone" target="_blank" rel="noopener" data-lia-auto-title-active="0" data-lia-auto-title="Baseline Microsoft Foundry Chat Reference Architecture in an Azure Landing Zone - Azure Architecture Center"&gt;Baseline Microsoft Foundry Chat Reference Architecture in an Azure Landing Zone - Azure Architecture Center&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A href="https://learn.microsoft.com/en-us/azure/web-application-firewall/afds/protect-azure-open-ai" target="_blank" rel="noopener"&gt;Protect Azure OpenAI using Azure Web Application Firewall on Azure Front Door — Microsoft Learn&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A href="https://learn.microsoft.com/en-us/azure/api-management/llm-token-limit-policy" target="_blank" rel="noopener"&gt;LLM token limit policy — Microsoft Learn&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A style="font-style: normal; font-weight: 400; background-color: rgb(255, 255, 255);" href="https://learn.microsoft.com/en-us/azure/api-management/llm-emit-token-metric-policy" target="_blank" rel="noopener"&gt;Emit token consumption metrics (llm-emit-token-metric) — Microsoft Learn&lt;/A&gt;&lt;/LI&gt;
&lt;/OL&gt;</description>
      <pubDate>Tue, 14 Jul 2026 08:19:35 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/from-ai-adoption-to-ai-governance-using-apim-as-the-gateway-for/ba-p/4536247</guid>
      <dc:creator>Abhishek_Mittal</dc:creator>
      <dc:date>2026-07-14T08:19:35Z</dc:date>
    </item>
    <item>
      <title>A Paradigm Shift in Cloud Operations with Azure SRE Agent</title>
      <link>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/a-paradigm-shift-in-cloud-operations-with-azure-sre-agent/ba-p/4533244</link>
      <description>&lt;P&gt;Cloud operations are entering a new era. As systems grow in scale and complexity, the traditional model of reactive incident response, where engineers manually piece together signals across dozens of tools and portals, juggling all that context alone, is no longer sustainable. The operational toil required to keep systems running shipping new capabilities.&lt;/P&gt;
&lt;P&gt;The question is straightforward: &lt;EM&gt;what if engineers could spend most of their time building instead of maintaining?&lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;To help organizations make that shift, today we’re sharing how &lt;STRONG&gt;Zafin, Provation Medical, &lt;/STRONG&gt;and&lt;STRONG&gt; InEight &lt;/STRONG&gt;are rethinking cloud operations with Azure SRE Agent. The SRE Agent product team has been working side by side with these customers, embedding with their engineering teams to agentify their cloud operations. What follows is the story of that collaboration and the results it produced.&lt;/P&gt;
&lt;H2&gt;From hours to minutes across industries&lt;/H2&gt;
&lt;P&gt;We worked closely with &lt;STRONG&gt;Zafin&lt;/STRONG&gt; starting October last year. As the onboarding progressed, and Zafin’s scenarios became more sophisticated, &lt;STRONG&gt;Zafin’s&lt;/STRONG&gt; security team needed confidence that the agent would respect their access boundaries at scale. We worked closely to configure granular RBAC, and scoping needed for multi-user rollout.&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;EM&gt;“Azure SRE Agent transformed how we approach incident response. We’ve moved from fragmented signals and manual triage to an intelligence-driven model where agents collect evidence, classify issues, and recommend actions before our engineers even engage. We’ve taken incident triage from hours down to minutes, and we’re now expanding this automation across observability, health monitoring, and incident management. As an AI platform company serving tier 1 banks globally, that speed, accuracy, and enterprise-grade governance is exactly what we need.”&lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;— George K Mathew, SVP Cloud &amp;amp; Business Operations, Zafin&lt;/STRONG&gt;&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;We partnered with &lt;STRONG&gt;Provation&lt;/STRONG&gt; on initial onboarding, connecting Azure DevOps as an incident source so the agent could begin triaging production support tickets. From there, they expanded into proactive health checks on their own.&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;EM&gt;“&lt;/EM&gt;&lt;EM&gt;When software runs reliably, care teams can focus on their patients instead of technology. At Provation, a leading provider of clinical productivity software, we’re continuing to advance our AI-powered software development lifecycle with Azure SRE Agent, Microsoft’s AI-powered reliability service.&lt;BR /&gt;&lt;BR /&gt;When a support ticket comes in, Azure SRE Agent pulls together the context an engineer needs to understand what the system was doing, what code recently changed, likely contributing factors, and recommended next steps. That analysis drops directly into our team’s normal workflow. In its first month, Azure SRE Agent provided analysis for all of our production-related tickets. Instead of checking multiple locations, engineers can start with more context already in front of them, helping work move forward more consistently and efficiently.&lt;BR /&gt;&lt;BR /&gt;Azure SRE Agent also supports our development environments, helping engineers review emerging patterns earlier in the process and create follow-up work with measurable first-month results, contributing to more than a quarter of related investigation tickets during that period.&lt;BR /&gt;&lt;BR /&gt;That’s what AI-assisted software development looks like day to day at Provation: intelligent tools integrated into the systems our teams already use, so healthcare providers get a smoother experience from start to finish.&lt;/EM&gt;&lt;EM&gt;”&lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;— Paul Snider, CTO, Provation Medical&lt;/STRONG&gt;&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;With &lt;STRONG&gt;InEight&lt;/STRONG&gt;, our engagement started with an on-site workshop where the agent diagnosed a live production bug their team had been unable to reproduce. That result drove rapid expansion, and we collaborated closely as InEight scaled from one product to multiple products and teams in three months.&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;EM&gt;“&lt;/EM&gt;&lt;EM&gt;SRE Agent is helping InEight transform how engineering operates. By embedding AI into software delivery, reliability engineering, quality assurance, security, and operational workflows, we are reducing manual effort, accelerating delivery, improving stability, and creating a scalable foundation for future growth. Incident investigation is down 80 percent, build failure triage down 80 percent, and bug investigation down 67 percent. Azure SRE Agent is the only tool we have found that reasons across source code, live telemetry, and Azure infrastructure simultaneously, in a single conversation. For a company operating a large suite of integrated products on Azure, that capability is not incremental. It is transformational&lt;/EM&gt;&lt;EM&gt;.”&lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;— Jim Ellerbeck, Vice President of Technology, InEight&lt;/STRONG&gt;&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;H2&gt;Built on governance and memory&lt;/H2&gt;
&lt;P&gt;Faster resolution is the most visible outcome, but not the full story. The reason these customers trust the agent with production operations comes down to two things: governance and memory.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Governance &lt;/STRONG&gt;is what makes this level of autonomy possible. The agent explains what it intends to do and why before acting, and every interaction produces a full audit trail. Routine operations run autonomously; actions designated high-impact pause for in-workflow sign-off. VNet integration routes traffic through your own network - NSG rules, private DNS, and firewalls all apply - while least-privilege access and granular tool-level policies keep the agent operating under your rules.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Memory&lt;/STRONG&gt; is what makes the system compound. Every investigation captures root causes, resolution steps, team preferences, and operational patterns. That knowledge persists across conversations. New team members ramp faster. On-call quality stays consistent regardless of who is paged. The collective expertise of the team grows automatically and never leaves when people do.&lt;/P&gt;
&lt;H2&gt;From maintaining to building&lt;/H2&gt;
&lt;P&gt;The pattern emerging from these customers points to a fundamental shift in what it means to run services. Traditional operations are giving way to an agent-driven model where the cognitive burden of monitoring, diagnosing, and resolving issues is lifted.&lt;/P&gt;
&lt;P&gt;When the agent handles the investigative toil, captures institutional knowledge, and gets smarter with every interaction, engineering teams can redirect their energy toward building the next generation of products and services. The teams adopting this model are not just operating faster. They are innovating faster, because their best people are no longer trapped in reactive maintenance cycles.&lt;/P&gt;
&lt;P&gt;Azure SRE Agent is generally available.&lt;/P&gt;
&lt;P&gt;Visit&amp;nbsp;&lt;A href="https://sre.azure.com/" target="_blank" rel="noopener"&gt;https://sre.azure.com&lt;/A&gt; to create your first agent in minutes.&lt;/P&gt;</description>
      <pubDate>Wed, 08 Jul 2026 22:41:52 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/a-paradigm-shift-in-cloud-operations-with-azure-sre-agent/ba-p/4533244</guid>
      <dc:creator>Nir_Mashkowski</dc:creator>
      <dc:date>2026-07-08T22:41:52Z</dc:date>
    </item>
    <item>
      <title>Auditing and Telemetry for the Agent Governance Toolkit -  Getting Started with .NET Core</title>
      <link>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/auditing-and-telemetry-for-the-agent-governance-toolkit-getting/ba-p/4533073</link>
      <description>&lt;H2&gt;Introduction&lt;/H2&gt;
&lt;P&gt;We've entered an era where AI agents autonomously invoke tools — reading and writing files, calling APIs, querying databases. Convenient as this is, without a mechanism to control who can call what, and under what conditions, you can't put it into production. The &lt;STRONG&gt;Agent Governance Toolkit (AGT)&lt;/STRONG&gt;, open-sourced by Microsoft, is exactly the toolkit for embedding that "gatekeeper" into AI agents. This article walks through getting started with AGT in .NET (C#), based on the following repository:&lt;/P&gt;
&lt;P&gt;&lt;A class="lia-external-url" href="https://github.com/normalian/MyAGTSamples/tree/main/AGTAuditBlobTelemetryApp01" target="_blank"&gt;https://github.com/normalian/MyAGTSamples/tree/main/AGTAuditBlobTelemetryApp01&lt;/A&gt;&lt;/P&gt;
&lt;P&gt;This sample walks through implementation examples focused on &lt;STRONG&gt;production-oriented audit log collection&lt;/STRONG&gt; and &lt;STRONG&gt;telemetry integration&lt;/STRONG&gt;. For basic usage of things like Policies, please refer to the following repository:&lt;/P&gt;
&lt;P&gt;&lt;A class="lia-external-url" href="https://github.com/microsoft/agent-governance-toolkit" target="_blank"&gt;https://github.com/microsoft/agent-governance-toolkit&lt;/A&gt;&lt;/P&gt;
&lt;P&gt;In environments where AI agents make important decisions and execute tools, recording an audit trail of "what happened" and "when it happened" is critical. AGT can export governance events to external systems, integrating seamlessly with production infrastructure such as Azure Blob Storage and Application Insights.&lt;/P&gt;
&lt;H4&gt;The Importance of Auditing and Telemetry in Production&lt;/H4&gt;
&lt;P&gt;&lt;STRONG&gt;Meeting Regulatory Requirements&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;In industries like finance and healthcare, an audit trail of an AI system's decision-making process and execution details is often legally required.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Anomaly Detection and Post-Incident Response&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;By analyzing audit logs, you can detect signs of anomalous agent behavior and respond quickly.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Performance Analysis&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;Collecting telemetry data lets you identify latency bottlenecks and failure patterns in agent execution.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Compliance Reporting&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;Audit trails can serve as evidence for security audits and compliance reviews.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Overview of the AGTAuditBlobTelemetryApp01 Project&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;This sample project builds on the previous sample (AGTIdentityWithMAFApp02), adding the following:&lt;/P&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;&lt;table border="1" style="width: 100%; height: 173.333px; border-width: 1px;"&gt;&lt;colgroup&gt;&lt;col style="width: 50%" /&gt;&lt;col style="width: 50%" /&gt;&lt;/colgroup&gt;&lt;tbody&gt;&lt;tr style="height: 34.6667px;"&gt;&lt;td style="height: 34.6667px;"&gt;Feature&lt;/td&gt;&lt;td style="height: 34.6667px;"&gt;Description&lt;/td&gt;&lt;/tr&gt;&lt;tr style="height: 34.6667px;"&gt;&lt;td style="height: 34.6667px;"&gt;&lt;STRONG&gt;Blob auditing&lt;/STRONG&gt;: &lt;EM&gt;BlobAuditSink&lt;/EM&gt;&lt;/td&gt;&lt;td style="height: 34.6667px;"&gt;Persists every governance event as an append-only record to an Azure Blob Storage Append Blob.&lt;/td&gt;&lt;/tr&gt;&lt;tr style="height: 34.6667px;"&gt;&lt;td style="height: 34.6667px;"&gt;&lt;STRONG&gt;Telemetry integration&lt;/STRONG&gt;&lt;/td&gt;&lt;td style="height: 34.6667px;"&gt;Exports metrics, traces, and logs to Application Insights using&amp;nbsp;&lt;EM&gt;Azure.Monitor.OpenTelemetry.Exporter&lt;/EM&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style="height: 34.6667px;"&gt;&lt;td style="height: 34.6667px;"&gt;&lt;STRONG&gt;Direct Governance Evaluation&lt;/STRONG&gt;&lt;/td&gt;&lt;td style="height: 34.6667px;"&gt;
&lt;P&gt;A demo that directly evaluates allow/deny decisions for tool calls based on policy&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;tr style="height: 34.6667px;"&gt;&lt;td style="height: 34.6667px;"&gt;&amp;nbsp;&lt;STRONG&gt;Thread safety&lt;/STRONG&gt;&lt;/td&gt;&lt;td style="height: 34.6667px;"&gt;Uses &lt;EM&gt;SemaphoreSlim&lt;/EM&gt;&amp;nbsp;to protect against concurrent access from multiple threads&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/DIV&gt;
&lt;H3&gt;Project Structure&lt;/H3&gt;
&lt;LI-CODE lang="textile"&gt;AGTAuditBlobTelemetryApp01/
│   AGTAuditBlobTelemetryApp01.csproj
│   AppConfiguration.cs
│   BlobAuditSink.cs
│   Program.cs
│   README.md
└───policies
        default.yaml&lt;/LI-CODE&gt;
&lt;H3&gt;Architecture Overview&lt;/H3&gt;
&lt;img /&gt;
&lt;H3&gt;Data Flow&lt;/H3&gt;
&lt;OL&gt;
&lt;LI&gt;&lt;STRONG&gt;Microsoft Agent Framework agent execution&lt;/STRONG&gt;: a tool call occurs&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Governance decision&lt;/STRONG&gt;: the GovernanceKernel decides allow/deny based on policy&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Event firing&lt;/STRONG&gt;: a &lt;EM&gt;GovernanceEvent&lt;/EM&gt;&amp;nbsp;is generated in memory&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Audit output (path ①)&lt;/STRONG&gt;: &lt;EM&gt;BlobAuditSink&lt;/EM&gt; appends the log to Blob in JSON format&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Telemetry output (path ②)&lt;/STRONG&gt;: a span is recorded via &lt;EM&gt;ActivitySource&lt;/EM&gt;&amp;nbsp;and exported to Application Insights&lt;/LI&gt;
&lt;/OL&gt;
&lt;H2&gt;Key Components Explained&lt;/H2&gt;
&lt;H3&gt;1. The BlobAuditSink Class&lt;/H3&gt;
&lt;P&gt;A component that records audit logs to an&amp;nbsp;&lt;STRONG&gt;Append Blob&lt;/STRONG&gt;&amp;nbsp;in Azure Blob Storage.&lt;/P&gt;
&lt;LI-CODE lang="csharp"&gt;internal sealed class BlobAuditSink : IDisposable
{
    private readonly AppendBlobClient _appendBlobClient;
    private readonly SemaphoreSlim _gate = new(1, 1);
    private bool _initialized;

    // TODO: Use Managed Identity in production
    public BlobAuditSink(string storageAccountUri, AzureCliCredential credential, 
                         string containerName, string blobName)
    {
        // Initialize the Blob container client
        var serviceClient = new BlobServiceClient(new Uri(storageAccountUri), credential);
        var containerClient = serviceClient.GetBlobContainerClient(containerName);
        containerClient.CreateIfNotExists();
        _appendBlobClient = containerClient.GetAppendBlobClient(blobName);
    }

    public void Append(GovernanceEvent governanceEvent)
    {
        // Append the governance event to the Blob in JSON format
        var record = new GovernanceAuditRecord(
            governanceEvent.EventId,
            governanceEvent.Timestamp,
            governanceEvent.Type.ToString(),
            governanceEvent.AgentId,
            governanceEvent.SessionId,
            governanceEvent.PolicyName,
            new Dictionary&amp;lt;string, object&amp;gt;(governanceEvent.Data));

        var payload = JsonSerializer.Serialize(record) + Environment.NewLine;
        var bytes = Encoding.UTF8.GetBytes(payload);

        _gate.Wait();  // Thread safety: only one thread may append to the Blob at a time
        try
        {
            EnsureBlobExists();
            using var stream = new MemoryStream(bytes, writable: false);
            _appendBlobClient.AppendBlock(stream);
        }
        finally
        {
            _gate.Release();
        }
    }
}&lt;/LI-CODE&gt;
&lt;H3&gt;Key Points&lt;/H3&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;SemaphoreSlim&lt;/STRONG&gt;: Controls concurrent access from multiple threads, restricting append operations on the Append Blob to a single thread at a time&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;AppendBlobClient&lt;/STRONG&gt;: Rather than traditional Upload/Download, this always appends to the end — ideal for log files&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;JSONL format&lt;/STRONG&gt;: one line = one record, which suits streaming processing and ingestion into spreadsheets. This can also be ingested into data platforms such as Microsoft Fabric.&lt;/LI&gt;
&lt;LI&gt;⚠️ &lt;STRONG&gt;Security&lt;/STRONG&gt;: Tool arguments may end up containing API keys or personal information. See the "&lt;STRONG&gt;Security Considerations: Sanitizing Audit Logs&lt;/STRONG&gt;" section below for details.&lt;/LI&gt;
&lt;/UL&gt;
&lt;H3&gt;2. OpenTelemetry Integration&lt;/H3&gt;
&lt;LI-CODE lang="csharp"&gt;using var meterProvider = Sdk.CreateMeterProviderBuilder()
    .SetResourceBuilder(resourceBuilder)
    .AddMeter(GovernanceMetrics.MeterName)
    .AddAzureMonitorMetricExporter(options =&amp;gt;
    {
        options.ConnectionString = applicationInsightsConnectionString;
    })
    .Build();

using var tracerProvider = Sdk.CreateTracerProviderBuilder()
    .SetResourceBuilder(resourceBuilder)
    .AddSource(GovernanceMetrics.MeterName)
    .AddSource(ActivitySource.Name)
    .AddAzureMonitorTraceExporter(options =&amp;gt;
    {
        options.ConnectionString = applicationInsightsConnectionString;
    })
    .Build();&lt;/LI-CODE&gt;
&lt;H4&gt;Key Points&lt;/H4&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;MeterProvider&lt;/STRONG&gt;: sends metrics from&amp;nbsp;&lt;EM&gt;GovernanceMetrics.MeterName&lt;/EM&gt; (= "&lt;EM&gt;agent_governance&lt;/EM&gt;") to Azure Monitor&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;TracerProvider&lt;/STRONG&gt;: records spans generated by &lt;EM&gt;ActivitySource&lt;/EM&gt;&amp;nbsp;into Application Insights&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;ResourceBuilder&lt;/STRONG&gt;: attaches a service name and version, making it possible to distinguish between multiple agents&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2&gt;3. The Governance Event Audit Hook&lt;/H2&gt;
&lt;LI-CODE lang="csharp"&gt;kernel.OnAllEvents(evt =&amp;gt;
{
    // Create a span: recorded to Application Insights as trace information
    using var activity = ActivitySource.StartActivity($"GovernanceEvent.{evt.Type}", 
                                                      ActivityKind.Internal);
    activity?.SetTag("event.type", evt.Type.ToString());
    activity?.SetTag("agent.id", evt.AgentId);
    activity?.SetTag("policy.name", evt.PolicyName ?? "(none)");
    activity?.SetTag("tool.name", evt.Data.GetValueOrDefault("tool_name", "(unknown)"));

    // Append the audit log to Blob Storage
    auditSink.Append(evt);
    
    // Emit a log entry
    logger.LogInformation(
        "[Audit -&amp;gt; Blob] {EventType} policy={PolicyName} tool={ToolName} agentId={AgentId}",
        evt.Type,
        evt.PolicyName ?? "(none)",
        evt.Data.GetValueOrDefault("tool_name", "(unknown)"),
        evt.AgentId);
});&lt;/LI-CODE&gt;
&lt;H4&gt;Key Points&lt;/H4&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;OnAllEvents callback&lt;/STRONG&gt;: captures every governance event (PolicyCheck, PolicyViolation, ToolCallBlocked, etc.)&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Dual output&lt;/STRONG&gt;: written simultaneously to Blob Storage (audit trail) and Application Insights (analytics telemetry)&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Span tags&lt;/STRONG&gt;: allow later filtering and grouping within Application Insights&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2&gt;4. Direct Governance Evaluation&lt;/H2&gt;
&lt;P&gt;A demo that judges allow/deny purely based on policy, without actually executing the tool call.&lt;/P&gt;
&lt;LI-CODE lang="csharp"&gt;// Allow example
var allowed = kernel.EvaluateToolCall(
    agent.Did,
    "GetLocation",
    new Dictionary&amp;lt;string, object&amp;gt;
    {
        ["city"] = "London"
    });

Console.WriteLine($"GetLocation call allowed? {allowed.Allowed}");
Console.WriteLine($"GetLocation call reason: {allowed.Reason}");

// Deny example
var blocked = kernel.EvaluateToolCall(
    agent.Did,
    "execute_shell",
    new Dictionary&amp;lt;string, object&amp;gt;
    {
        ["cmd"] = "rm -rf /"
    });

Console.WriteLine($"Blocked call allowed? {blocked.Allowed}");
Console.WriteLine($"Blocked call reason: {blocked.Reason}");&lt;/LI-CODE&gt;
&lt;H4&gt;Key Points&lt;/H4&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Pre-execution decision&lt;/STRONG&gt;: can be evaluated before the tool is actually run — efficient for things like log processing&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Detailed reasoning&lt;/STRONG&gt;: allowed.Reason lets you see exactly which policy rule produced the decision&lt;/LI&gt;
&lt;/UL&gt;
&lt;H4&gt;Governance Policy Explained&lt;/H4&gt;
&lt;LI-CODE lang="yaml"&gt;apiVersion: governance.toolkit/v1
version: "1.0"
name: audit-blob-telemetry-policy
default_action: deny

rules:
  - name: allow-weather
    condition: "tool_name == 'GetWeather'"
    action: allow
    priority: 10

  - name: allow-time
    condition: "tool_name == 'GetTime'"
    action: allow
    priority: 10

  - name: allow-location
    condition: "tool_name == 'GetLocation'"
    action: allow
    priority: 10

  - name: block-shell
    condition: "tool_name == 'execute_shell'"
    action: deny
    priority: 100&lt;/LI-CODE&gt;
&lt;H4&gt;Policy Strategy&lt;/H4&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Default action: deny&lt;/STRONG&gt;: any tool not explicitly allowed is denied (a whitelist approach)&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Priority-based rules&lt;/STRONG&gt;: higher priority is evaluated first.&amp;nbsp;&lt;EM&gt;block-shell&lt;/EM&gt;&amp;nbsp;has priority 100, giving it an explicit deny&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2&gt;Run the Sample&lt;/H2&gt;
&lt;H4&gt;Prerequisites&lt;/H4&gt;
&lt;P&gt;Set the following environment variables:&lt;/P&gt;
&lt;LI-CODE lang="powershell"&gt;$env:AZURE_OPENAI_ENDPOINT = "https://your-resource.openai.azure.com/"
$env:AZURE_OPENAI_DEPLOYMENT = "gpt-5.4-mini"
$env:APPLICATIONINSIGHTS_CONNECTION_STRING = "InstrumentationKey=...;..."
$env:AZURE_STORAGE_ACCOUNT_NAME = "yourstorageaccount"
$env:AUDIT_STORAGE_CONTAINER = "agt-audit"&lt;/LI-CODE&gt;
&lt;H4&gt;Execution Result&lt;/H4&gt;
&lt;LI-CODE lang="textile"&gt;==================================================
AGT Audit to Blob + Telemetry to Application Insights
==================================================

Agent DID: did:mesh:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Agent Status: Active
Telemetry: Application Insights via AzureMonitorExporterOptions
Audit: Azure Blob Storage container 'agt-audit'

✓ OpenTelemetry providers initialized
- Metrics: agent_governance
- Traces: AgentGovernance.Examples.AuditBlobTelemetry
- Logs: AgentGovernance.Examples

=== Agent Run 1 ===
[Audit -&amp;gt; Blob] PolicyCheck policy= tool=GetWeather agentId=did:mesh:...
Role: assistant
FunctionCall: GetWeather({"city":"Seattle"})
Role: tool
FunctionResult: call_xxx = Seattle is sunny, 22°C
Role: assistant
Text: The weather in Seattle is sunny with a temperature of 22°C.

=== Agent Run 2 ===
[Audit -&amp;gt; Blob] PolicyCheck policy= tool=GetTime agentId=did:mesh:...
Role: assistant
FunctionCall: GetTime({"timezone":"Tokyo"})
Role: tool
FunctionResult: call_yyy = Current time in Tokyo: 09:30:00 UTC
Role: assistant
Text: The current time in Tokyo is 09:30:00 UTC.

=== Direct Governance Demo - Allow Example ===
GetLocation call allowed? True
GetLocation call reason: Matches allow-location rule

=== Direct Governance Demo - Deny Example ===
Blocked call allowed? False
Blocked call reason: Matches block-shell rule (deny)

=== Flushing Telemetry to Application Insights ===

Flushing metrics...
Flushing traces...
Flushing logs...
Waiting 5 seconds for data transmission...
✓ Telemetry flush complete.&lt;/LI-CODE&gt;
&lt;H4&gt;Security Considerations: Sanitizing Audit Logs&lt;/H4&gt;
&lt;P&gt;In production, tool arguments may end up containing &lt;STRONG&gt;API keys, passwords, or personally identifiable information (PII)&lt;/STRONG&gt;. Because of this, it's important to apply masking before writing to Blob.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Masking Strategy:&lt;/STRONG&gt;&lt;/P&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;&lt;table border="1" style="width: 100%; height: 173.333px; border-width: 1px;"&gt;&lt;colgroup&gt;&lt;col style="width: 33.3333%" /&gt;&lt;col style="width: 33.3333%" /&gt;&lt;col style="width: 33.3333%" /&gt;&lt;/colgroup&gt;&lt;tbody&gt;&lt;tr style="height: 34.6667px;"&gt;&lt;td style="height: 34.6667px;"&gt;&lt;STRONG&gt;Target Key&lt;/STRONG&gt;&lt;/td&gt;&lt;td style="height: 34.6667px;"&gt;&lt;STRONG&gt;Masking Example&lt;/STRONG&gt;&lt;/td&gt;&lt;td style="height: 34.6667px;"&gt;&lt;STRONG&gt;Description&lt;/STRONG&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style="height: 34.6667px;"&gt;&lt;td style="height: 34.6667px;"&gt;&lt;EM&gt;api_key&lt;/EM&gt;, &lt;EM&gt;token&lt;/EM&gt;&lt;/td&gt;&lt;td style="height: 34.6667px;"&gt;sk***...d7e2&lt;/td&gt;&lt;td style="height: 34.6667px;"&gt;Show only the first and last 2 characters&lt;/td&gt;&lt;/tr&gt;&lt;tr style="height: 34.6667px;"&gt;&lt;td style="height: 34.6667px;"&gt;&lt;EM&gt;password&lt;/EM&gt;, &lt;EM&gt;secret&lt;/EM&gt;&lt;/td&gt;&lt;td style="height: 34.6667px;"&gt;***&lt;/td&gt;&lt;td style="height: 34.6667px;"&gt;Fully hidden&lt;/td&gt;&lt;/tr&gt;&lt;tr style="height: 34.6667px;"&gt;&lt;td style="height: 34.6667px;"&gt;&lt;EM&gt;email&lt;/EM&gt;&lt;/td&gt;&lt;td style="height: 34.6667px;"&gt;us***...com&lt;/td&gt;&lt;td style="height: 34.6667px;"&gt;Mask the middle of the email address&lt;/td&gt;&lt;/tr&gt;&lt;tr style="height: 34.6667px;"&gt;&lt;td style="height: 34.6667px;"&gt;&lt;EM&gt;cmd&lt;/EM&gt; (shell command)&lt;/td&gt;&lt;td style="height: 34.6667px;"&gt;Excluded from masking, since knowing whether it was executed matters&lt;/td&gt;&lt;td style="height: 34.6667px;"&gt;However, treat separately if the arguments contain sensitive data&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/DIV&gt;
&lt;H2&gt;&amp;nbsp;Checking Blob Storage Audit Logs&lt;/H2&gt;
&lt;P&gt;Open the Blob Storage account in the Azure Portal and check the `governance-audit-YYYYMMDD.jsonl` file in the `agt-audit` container.&lt;/P&gt;
&lt;P&gt;Example content:&lt;/P&gt;
&lt;LI-CODE lang="jsonp"&gt;{"EventId":"evt-42a6f9d598c44d56b15152699ac8eafa","Timestamp":"2026-07-02T01:47:32.0371686+00:00","Type":"PolicyViolation","AgentId":"did:agentmesh:weatheragent","SessionId":"maf-e2ed4a320f944bbf","PolicyName":null,"Data":{"message":"What is the weather in Seattle?","allowed":false,"action":"deny","reason":"No matching rules; default action is deny.","evaluation_ms":37.2383}}
{"EventId":"evt-7ff65a1c8bd84195a26722adc20ce2bf","Timestamp":"2026-07-02T01:47:41.1239754+00:00","Type":"PolicyViolation","AgentId":"did:agentmesh:weatheragent","SessionId":"maf-9f949b3838054324","PolicyName":null,"Data":{"message":"What time is it in Tokyo?","allowed":false,"action":"deny","reason":"No matching rules; default action is deny.","evaluation_ms":0.1392}}
{"EventId":"evt-baf03404cd8a477eaaa5e0af93413a74","Timestamp":"2026-07-02T01:47:48.501694+00:00","Type":"PolicyViolation","AgentId":"did:agentmesh:weatheragent","SessionId":"maf-e88d7382ed3b49a0","PolicyName":null,"Data":{"message":"Where is Paris located?","allowed":false,"action":"deny","reason":"No matching rules; default action is deny.","evaluation_ms":0.0919}}
{"EventId":"evt-107e6c8e18e14fdcb8272844e57c45b1","Timestamp":"2026-07-02T01:47:53.9755476+00:00","Type":"PolicyCheck","AgentId":"did:mesh:4c3ca93fec8b9d78fd05839694ed7df8","SessionId":"session-775002743f0f4d28","PolicyName":"allow-location","Data":{"tool_name":"GetLocation","allowed":true,"action":"allow","reason":"Matched rule \u0027allow-location\u0027 with action \u0027Allow\u0027.","evaluation_ms":4.3211,"arguments":{"city":"London"}}}
{"EventId":"evt-6bc7e6b8632a41cb885fc5e87097c6ac","Timestamp":"2026-07-02T01:48:02.4498334+00:00","Type":"ToolCallBlocked","AgentId":"did:mesh:4c3ca93fec8b9d78fd05839694ed7df8","SessionId":"session-bfda81e9ce30429a","PolicyName":"block-shell","Data":{"tool_name":"execute_shell","allowed":false,"action":"deny","reason":"Matched rule \u0027block-shell\u0027 with action \u0027Deny\u0027.","evaluation_ms":0.065,"arguments":{"cmd":"rm -rf /"}}}&lt;/LI-CODE&gt;
&lt;H4&gt;How to Use It&lt;/H4&gt;
&lt;P&gt;&lt;STRONG&gt;KQL Query — Kusto Query Language&lt;/STRONG&gt;** — analyze logs like this:&lt;/P&gt;
&lt;LI-CODE lang="kusto"&gt;// Aggregate allow/deny counts for metrics under agent_governance.
AppMetrics
| where Name startswith "agent_governance."
| where TimeGenerated &amp;gt; ago(24h)
| extend Decision = tostring(Properties.decision)
| summarize Count = sum(Sum) by Name, Decision
| order by Name asc, Decision asc&lt;/LI-CODE&gt;&lt;img /&gt;
&lt;H4&gt;Checking in Application Insights&lt;/H4&gt;
&lt;P&gt;In the Azure Portal, go to Application Insights →&amp;nbsp;&lt;STRONG&gt;Search&lt;/STRONG&gt; → &lt;STRONG&gt;Traces&lt;/STRONG&gt; tab to check the agent's execution performance.&lt;/P&gt;
&lt;img /&gt;
&lt;H2&gt;Real-World Usage Scenarios&lt;/H2&gt;
&lt;H4&gt;&lt;STRONG&gt;Scenario 1&lt;/STRONG&gt;: Detecting Anomalous Access Patterns&lt;/H4&gt;
&lt;LI-CODE lang="csharp"&gt;// Raise an alert if a given agent receives many tool denials in a short time window
// Note: since PolicyName can be null, combine a Type-based check with null-safe checks
var recentDenials = auditLogs
    .Where(l =&amp;gt; l.Timestamp &amp;gt; DateTime.UtcNow.AddMinutes(-5))
    .Where(l =&amp;gt; l.Type is "PolicyViolation" or "ToolCallBlocked"
             || (l.PolicyName?.Contains("deny") ?? false)
             || (l.PolicyName?.Contains("block") ?? false))
    .GroupBy(l =&amp;gt; l.AgentId)
    .Where(g =&amp;gt; g.Count() &amp;gt; 10);

foreach (var agentGroup in recentDenials)
{
    NotifySecurityTeam($"Agent {agentGroup.Key} attempted " +
        $"{agentGroup.Count()} forbidden operations in the last 5 minutes");
}&lt;/LI-CODE&gt;
&lt;P&gt;&lt;STRONG&gt;Notes:&lt;/STRONG&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Type-based checks&lt;/STRONG&gt;: as the JSONL sample shows, default-deny&amp;nbsp;&lt;EM&gt;PolicyViolation&lt;/EM&gt; entries have a &lt;EM&gt;PolicyName&lt;/EM&gt; of &lt;EM&gt;null&lt;/EM&gt;. Making the Type-based check the primary criterion avoids exceptions from missing null checks.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Null-safe operators&lt;/STRONG&gt;: combining&amp;nbsp;&lt;EM&gt;PolicyName?.Contains()&lt;/EM&gt; with &lt;EM&gt;?? false&lt;/EM&gt;&amp;nbsp;avoids null-reference exceptions while still allowing policy-name-based filtering.&lt;/LI&gt;
&lt;/UL&gt;
&lt;H4&gt;Scenario 2: Agent Behavior Monitoring Dashboard&lt;/H4&gt;
&lt;P&gt;Build a custom Application Insights dashboard that shows, in real time:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Agent count&lt;/STRONG&gt;: number of currently active agents&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Denial rate&lt;/STRONG&gt;: percentage of policy denials over the past hour&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Average response time&lt;/STRONG&gt;: average latency of tool calls&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Error rate&lt;/STRONG&gt;: percentage of tool calls that raised an exception&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2&gt;Considerations for Production&lt;/H2&gt;
&lt;H4&gt;1. Scalability&lt;/H4&gt;
&lt;P&gt;At large agent-fleet scale, writes to Blob Storage may become a bottleneck.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Mitigations&lt;/STRONG&gt;:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Hierarchical partitioning with Azure Data Lake Storage (ADLS)&lt;/LI&gt;
&lt;LI&gt;Batch processing via Event Hubs&lt;/LI&gt;
&lt;LI&gt;Load-balancing writes across multiple Append Blobs&lt;/LI&gt;
&lt;/UL&gt;
&lt;H4&gt;2. Cost Optimization&lt;/H4&gt;
&lt;P&gt;Storing large volumes of logs in Blob Storage increases storage costs.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Mitigations&lt;/STRONG&gt;:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Automatic deletion of old logs (lifecycle policies)&lt;/LI&gt;
&lt;LI&gt;Staged tiering from Hot → Cool → Archive&lt;/LI&gt;
&lt;LI&gt;Storing only the information you actually need (filtering)&lt;/LI&gt;
&lt;/UL&gt;
&lt;H4&gt;3. Performance&lt;/H4&gt;
&lt;P&gt;Emitting telemetry to Application Insights may add latency.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Mitigations&lt;/STRONG&gt;:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Use batch processing mode (the default)&lt;/LI&gt;
&lt;LI&gt;Limit the number of traces (sampling)&lt;/LI&gt;
&lt;LI&gt;Asynchronous output&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2&gt;Summary&lt;/H2&gt;
&lt;P&gt;Integrating AGT's auditing and telemetry gives you:&lt;/P&gt;
&lt;OL&gt;
&lt;LI&gt;&lt;STRONG&gt;A complete audit trail&lt;/STRONG&gt;: every policy decision and execution result is recorded&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Real-time monitoring&lt;/STRONG&gt;: agent behavior visualized through Application Insights&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Anomaly detection&lt;/STRONG&gt;: early warning through pattern recognition&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Production readiness&lt;/STRONG&gt;: a scalable, robust architecture&lt;/LI&gt;
&lt;/OL&gt;
&lt;P&gt;In production environments where AI agents play a critical role, &lt;STRONG&gt;transparency&lt;/STRONG&gt; and &lt;STRONG&gt;traceability&lt;/STRONG&gt; are essential. By leveraging AGT's auditing and telemetry features, you can achieve safe, reliable agent operations.&lt;/P&gt;
&lt;H2&gt;References&lt;/H2&gt;
&lt;UL&gt;
&lt;LI&gt;Agent Governance Toolkit - GitHub &lt;A class="lia-external-url" href="https://github.com/microsoft/agent-governance-toolkit" target="_blank"&gt;https://github.com/microsoft/agent-governance-toolkit&lt;/A&gt; &amp;nbsp;&lt;/LI&gt;
&lt;LI&gt;Azure Blob Storage - Append Blob &lt;A class="lia-external-url" href="https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blobs-append" target="_blank"&gt;https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blobs-append&lt;/A&gt; &amp;nbsp;&lt;/LI&gt;
&lt;LI&gt;Azure Monitor OpenTelemetry Exporter &lt;A class="lia-external-url" href="https://learn.microsoft.com/en-us/azure/azure-monitor/app/opentelemetry-enable" target="_blank"&gt;https://learn.microsoft.com/en-us/azure/azure-monitor/app/opentelemetry-enable&lt;/A&gt; &amp;nbsp;&lt;/LI&gt;
&lt;LI&gt;Application Insights - KQL Query Reference &lt;A class="lia-external-url" href="https://learn.microsoft.com/en-us/azure/azure-monitor/logs/kql-quick-reference" target="_blank"&gt;https://learn.microsoft.com/en-us/azure/azure-monitor/logs/kql-quick-reference&lt;/A&gt; &amp;nbsp;&lt;/LI&gt;
&lt;LI&gt;Sample code repository &lt;A class="lia-external-url" href="https://github.com/normalian/MyAGTSamples" target="_blank"&gt;https://github.com/normalian/MyAGTSamples&lt;/A&gt;&amp;nbsp;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;I hope this article helps with implementing audit and telemetry capabilities for AI agents in production. If you have any questions or feedback, please let me know in the comments!&lt;/P&gt;</description>
      <pubDate>Thu, 02 Jul 2026 13:35:00 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/auditing-and-telemetry-for-the-agent-governance-toolkit-getting/ba-p/4533073</guid>
      <dc:creator>daisami</dc:creator>
      <dc:date>2026-07-02T13:35:00Z</dc:date>
    </item>
    <item>
      <title>How to build long-running MCP tools on Azure Functions</title>
      <link>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/how-to-build-long-running-mcp-tools-on-azure-functions/ba-p/4532653</link>
      <description>&lt;P&gt;Recently, a customer building servers with the &lt;A class="lia-internal-link lia-internal-url lia-internal-url-content-type-blog" href="https://techcommunity.microsoft.com/blog/appsonazureblog/azure-functions-mcp-extension-whats-new-at-build-2026/4524099" data-lia-auto-title="Azure Functions MCP extension" data-lia-auto-title-active="0" target="_blank"&gt;Azure Functions MCP extension&lt;/A&gt; reached out and asked:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;EM&gt;How do I handle tools that take longer than the client is willing to wait?&lt;/EM&gt;&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;P&gt;This becomes especially relevant when tool calls move beyond simple request/response into multi-step workflows and long-running operations.&lt;/P&gt;
&lt;P&gt;At the same time, MCP is evolving to address exactly this. The&amp;nbsp;&lt;A href="https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/#tasks-graduates-to-an-extension" target="_blank"&gt;Tasks extension&lt;/A&gt;&amp;nbsp;is introduced in the &lt;A class="lia-external-url" href="https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/" target="_blank"&gt;2026-07-28 release candidate&lt;/A&gt;, defining a standard way to model long-running work.&lt;/P&gt;
&lt;P&gt;In this post, we’ll walk through how to build long-running MCP tools on Azure Functions using &lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/durable-task/durable-functions/durable-functions-overview?tabs=csharp" target="_blank"&gt;Durable Functions&lt;/A&gt; , a framework for authoring stateful, long-running workflows as ordinary code, with checkpointing, scaling, and recovery handled automatically.&amp;nbsp;&lt;/P&gt;
&lt;H2&gt;MCP tools today&lt;/H2&gt;
&lt;P&gt;Today, MCP tools are fundamentally request/response:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;the client issues a &lt;STRONG&gt;tools/call&lt;/STRONG&gt;&lt;/LI&gt;
&lt;LI&gt;the server returns a result&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;This works well for fast operations, but breaks down when:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;workflows take minutes&lt;/LI&gt;
&lt;LI&gt;execution depends on multiple steps&lt;/LI&gt;
&lt;LI&gt;latency is unpredictable&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;In practice, clients enforce their own tool-call timeouts. These aren't standardized by the MCP spec and vary per client, but they're often in the ~30–60 second range. If a tool exceeds that window:&lt;/P&gt;
&lt;P&gt;In practice, clients often enforce short timeouts. If a tool exceeds that window:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;the client times out&lt;/LI&gt;
&lt;LI&gt;the agent observes a failed call&lt;/LI&gt;
&lt;LI&gt;the underlying work may still be running&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;So the core issue is that you have synchronous tool calls don’t naturally model long-running work.&lt;/P&gt;
&lt;H2&gt;The MCP Tasks extension&lt;/H2&gt;
&lt;P&gt;The &lt;A class="lia-external-url" href="https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/#tasks-graduates-to-an-extension" target="_blank"&gt;Tasks extension&lt;/A&gt; to address this. With the extension, a server can respond to a &lt;STRONG&gt;tools/call&lt;/STRONG&gt; with an asynchronous&amp;nbsp;&lt;EM&gt;task handle&lt;/EM&gt;&amp;nbsp;instead of a final result, and the client drives the lifecycle from there:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;tasks/get: &lt;/STRONG&gt;poll the task's status&amp;nbsp;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;tasks/update:&lt;/STRONG&gt;&amp;nbsp;submit input back to the server if the task reaches input_required&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;tasks/cancel:&lt;/STRONG&gt;&amp;nbsp;cancel an in-flight task&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;A task carries a status ("working", "input_required", "completed", "failed", or "cancelled") and on completion, the final result. Task creation is &lt;EM&gt;server-directed&lt;/EM&gt;: the client advertises support by including the extension in its per-request capabilities, and the server decides per request whether to return a task. A server won't return a task to a client that hasn't advertised support.&lt;/P&gt;
&lt;P&gt;It's important to note that Tasks rely on ecosystem support. Clients must advertise the extension, and MCP SDKs must implement the task lifecycle, before servers can use it. So while Tasks is now a defined extension, broad client and SDK support is still in progress.&lt;/P&gt;
&lt;H2&gt;Implement long-runng tasks with Durable Functions today&lt;/H2&gt;
&lt;P&gt;Until the Tasks extension is broadly supported across clients, we need a pattern that works with existing request/response clients and supports long-running execution. The following samples show how, using Durable Functions:&amp;nbsp;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;A class="lia-external-url" href="https://github.com/Azure-Samples/mcp-functions-long-running-tools-python" target="_blank"&gt;Python&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A class="lia-external-url" href="https://github.com/Azure-Samples/mcp-functions-long-running-tools" target="_blank"&gt;NET&lt;/A&gt;&amp;nbsp;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;The long-running work in this sample mines a short chain of blocks. Each block requires solving a computational puzzle where the system keeps trying different inputs until it finds one that produces a result matching a specific pattern (for example, starting with a certain number of zeros). Because this involves lots of trial and error, it naturally takes time, making it a good example of a long-running workflow.&amp;nbsp;&lt;/P&gt;
&lt;P&gt;The server in the sample exposes two tools:&lt;/P&gt;
&lt;OL&gt;
&lt;LI&gt;&lt;STRONG&gt; start_mining&lt;/STRONG&gt;&lt;/LI&gt;
&lt;/OL&gt;
&lt;UL&gt;
&lt;LI&gt;Starts a Durable Functions orchestration to mine the blocks&lt;/LI&gt;
&lt;LI&gt;Waits briefly (within a configurable budget)&lt;/LI&gt;
&lt;LI&gt;Returns result inline if completed within budget OR returns &lt;EM&gt;workflow_id &lt;/EM&gt;if still running&lt;/LI&gt;
&lt;/UL&gt;
&lt;OL start="2"&gt;
&lt;LI&gt;&lt;STRONG&gt; get_mining_result&lt;/STRONG&gt;&lt;/LI&gt;
&lt;/OL&gt;
&lt;UL&gt;
&lt;LI&gt;Takes the &lt;EM&gt;workflow_id&lt;/EM&gt;&lt;/LI&gt;
&lt;LI&gt;&amp;nbsp;Returns the current state, e.g. "completed", "running", "failed", or "not_found"&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;To ensure that the agent calls the tools in the right order, &lt;EM&gt;workflow_id&lt;/EM&gt; is a required parameter of &lt;STRONG&gt;get_mining_result&lt;/STRONG&gt;, so the agent can't poll without starting a mining run first. Also, the "running" response carries a&amp;nbsp;&lt;EM&gt;poll_after_seconds&lt;/EM&gt; and a &lt;EM&gt;next&lt;/EM&gt; instruction, ensuring the agent to poll again if work is not done rather than give up or assume completion.&amp;nbsp;&lt;/P&gt;
&lt;P&gt;Even so, the poll path still relies on the agent correctly remembering, and not hallucinating, the &lt;EM&gt;workflow_id&lt;/EM&gt; it was handed. If it garbles or invents an id, the poll lands on the wrong instance or none at all (which is why &lt;STRONG&gt;get_mining_result&lt;/STRONG&gt; returns "not_found" rather than guessing).&lt;/P&gt;
&lt;H4&gt;What changes with the Tasks extension&lt;/H4&gt;
&lt;P&gt;Once the Tasks extension is fully implemented across clients and SDKs, the model becomes simpler and more reliable: the server returns a&lt;STRONG&gt; &lt;/STRONG&gt;Task handle, the client manages the polling and lifecyle calls, and the SDK tracks execution state. This removes a key limitation of today’s solution, which requires the agent to remember and correctly pass identifiers like &lt;EM&gt;workflow_id.&lt;/EM&gt;&lt;/P&gt;
&lt;H2&gt;Call to action&lt;/H2&gt;
&lt;P&gt;Try out the sample and let us know whether it addresses your MCP needs around long-running or workflow type tools!&amp;nbsp;&lt;/P&gt;</description>
      <pubDate>Wed, 01 Jul 2026 16:10:42 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/how-to-build-long-running-mcp-tools-on-azure-functions/ba-p/4532653</guid>
      <dc:creator>lily-ma</dc:creator>
      <dc:date>2026-07-01T16:10:42Z</dc:date>
    </item>
    <item>
      <title>Azure Container Apps Express for Shipping Container Apps Fast</title>
      <link>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/azure-container-apps-express-for-shipping-container-apps-fast/ba-p/4531371</link>
      <description>&lt;P&gt;&lt;STRONG&gt;ACA Express Apps are a strong fit for teams that need to ship quickly and can't afford long platform setup cycles.&lt;/STRONG&gt; This includes startups, internal platform teams, and product groups deploying APIs, web apps, or agent endpoints that scale with uneven demand. If the priority is &lt;STRONG&gt;fast path-to-production&lt;/STRONG&gt;, predictable &lt;STRONG&gt;wake-up&lt;/STRONG&gt; behavior, and &lt;STRONG&gt;minimal infrastructure overhead&lt;/STRONG&gt;, this model is likely the right choice.&lt;/P&gt;
&lt;P&gt;To put real numbers behind that, I built a live demo that races Express against a Consumption environment on the same app. The measurements below come from that demo, not from a spec sheet.&lt;/P&gt;
&lt;!-- INSERT VIDEO: Express Espresso demo walkthrough — live app, cold-start race, and zero-to-URL boot race --&gt;
&lt;P class="lia-clear-both"&gt;&amp;nbsp;&lt;/P&gt;
&lt;div data-video-id="https://www.youtube.com/watch?v=jQ69L0AiwPY/1782840324487" data-video-remote-vid="https://www.youtube.com/watch?v=jQ69L0AiwPY/1782840324487" class="lia-video-container lia-media-is-center lia-media-size-large"&gt;&lt;iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FjQ69L0AiwPY%3Ffeature%3Doembed&amp;amp;display_name=YouTube&amp;amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DjQ69L0AiwPY&amp;amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FjQ69L0AiwPY%2Fhqdefault.jpg&amp;amp;type=text%2Fhtml&amp;amp;schema=youtube" allowfullscreen="" style="max-width: 100%"&gt;&lt;/iframe&gt;&lt;/div&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;H2&gt;MicroVMs make cold starts practical&lt;/H2&gt;
&lt;P&gt;Cold start delays usually come from rebuilding runtime state whenever an app wakes up. ACA Express Apps reduce that overhead with MicroVM-based startup paths built for &lt;STRONG&gt;fast boot and isolation&lt;/STRONG&gt;. The result is faster instance readiness without trading off security.&lt;/P&gt;
&lt;P&gt;The gap shows up clearly when both apps have scaled all the way to zero. Waking from a genuine cold start, Express comes back in about &lt;STRONG&gt;1.5 seconds&lt;/STRONG&gt;. The same app in a Consumption environment takes about &lt;STRONG&gt;20 seconds&lt;/STRONG&gt; to answer the first request. Both were measured live in the browser, from request to first response.&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;H2&gt;Disk and memory state restore is the speed multiplier&lt;/H2&gt;
&lt;P&gt;State restoration skips the app's internal boot sequence entirely. Instead of replaying the same initialization work on every start, ACA Express Apps can &lt;STRONG&gt;restore disk and memory state&lt;/STRONG&gt; so the app starts closer to ready. That reduces time-to-first-request and smooths scale events, especially for framework-heavy workloads. It's also what lets scale-to-zero stay practical: the app costs nothing while idle, but the wake-up penalty stays in the low single-digit seconds instead of the tens of seconds you'd otherwise pay.&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;H2&gt;Environmentless changes the deployment experience&lt;/H2&gt;
&lt;P&gt;Skipping the environment setup completely changes the deployment workflow. Teams can ship the container app without first managing environment sprawl, while still getting the runtime foundations they need. For fast-moving teams, that means &lt;STRONG&gt;less setup overhead and a shorter path to production&lt;/STRONG&gt;.&lt;/P&gt;
&lt;P&gt;You can see how little there is to fill in. Creating an Express app is a single short form. There is no environment to stand up first.&lt;/P&gt;
&lt;img /&gt;&lt;!-- INSERT IMAGE: Express app create page — one form, no environment setup step --&gt;
&lt;P&gt;And once it's created, the manage view gives you the live URL, status, and the basics you need to operate it.&lt;/P&gt;
&lt;!-- INSERT IMAGE: Express app manage page — the running app, live URL and status, ready to use --&gt;&lt;img /&gt;
&lt;H2&gt;The numbers, side by side&lt;/H2&gt;
&lt;P&gt;Everything below was measured on the same container image, in the West Central US region.&lt;/P&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;&lt;table border="1" style="border-width: 1px;"&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;STRONG&gt;What's measured&lt;/STRONG&gt;&lt;/th&gt;&lt;th&gt;&lt;STRONG&gt;Express&lt;/STRONG&gt;&lt;/th&gt;&lt;th&gt;&lt;STRONG&gt;Consumption&lt;/STRONG&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Cold start from zero (request to first response)&lt;/td&gt;&lt;td&gt;&lt;STRONG&gt;~1.5 s&lt;/STRONG&gt;&lt;/td&gt;&lt;td&gt;~20 s&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Environment provisioning&lt;/td&gt;&lt;td&gt;&lt;STRONG&gt;~14 s&lt;/STRONG&gt;&lt;/td&gt;&lt;td&gt;~120 s&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;First-time deploy (environment + app, zero to live URL)&lt;/td&gt;&lt;td&gt;&lt;STRONG&gt;~52 s&lt;/STRONG&gt;&lt;/td&gt;&lt;td&gt;~166 s&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;App deploy only (environment already exists)&lt;/td&gt;&lt;td&gt;~30 s&lt;/td&gt;&lt;td&gt;~30 s&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;colgroup&gt;&lt;col style="width: 33.33%" /&gt;&lt;col style="width: 33.33%" /&gt;&lt;col style="width: 33.33%" /&gt;&lt;/colgroup&gt;&lt;/table&gt;&lt;/DIV&gt;
&lt;P&gt;Express is much faster on the two steps that build infrastructure from scratch: cold start and environment provisioning. Once an environment already exists, the two are about the same. Express isn't a different app runtime, it's the same platform with the first-time setup cost stripped down.&lt;/P&gt;
&lt;H2&gt;Get started&lt;/H2&gt;
&lt;P&gt;Express is in public preview. You can have a container on a live URL in the time it takes to read this post.&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;📖 &lt;A href="https://learn.microsoft.com/en-us/azure/container-apps/express-overview" target="_blank" rel="noopener"&gt;&lt;STRONG&gt;Azure Container Apps Express overview&lt;/STRONG&gt;&lt;/A&gt; — concepts, capabilities, and the current feature support matrix.&lt;/LI&gt;
&lt;LI&gt;🚀 &lt;A href="https://learn.microsoft.com/en-us/azure/container-apps/express-faq#getting-started" target="_blank" rel="noopener"&gt;&lt;STRONG&gt;Create your first Express app&lt;/STRONG&gt;&lt;/A&gt; — the CLI commands and portal steps to get an app running.&lt;/LI&gt;
&lt;LI&gt;🛠️ &lt;A href="https://containerapps.azure.com/" target="_blank" rel="noopener"&gt;&lt;STRONG&gt;New Container Apps portal&lt;/STRONG&gt;&lt;/A&gt; — create and manage Express apps in the streamlined UI.&lt;/LI&gt;
&lt;LI&gt;🧪 &lt;A href="https://learn.microsoft.com/en-us/azure/container-apps/express-local-testing" target="_blank" rel="noopener"&gt;&lt;STRONG&gt;Test Express apps locally&lt;/STRONG&gt;&lt;/A&gt; — validate your container before you deploy.&lt;/LI&gt;
&lt;LI&gt;❓ &lt;A href="https://learn.microsoft.com/en-us/azure/container-apps/express-faq" target="_blank" rel="noopener"&gt;&lt;STRONG&gt;Express FAQ&lt;/STRONG&gt;&lt;/A&gt; — preview status, limits, regions, and how Express relates to standard Container Apps.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;👉 &lt;STRONG&gt;&lt;A href="https://containerapps.azure.com/" target="_blank" rel="noopener"&gt;Deploy an Express app&lt;/A&gt;&lt;/STRONG&gt; · &lt;STRONG&gt;&lt;A href="https://learn.microsoft.com/en-us/azure/container-apps/express-overview" target="_blank" rel="noopener"&gt;Read the docs&lt;/A&gt;&lt;/STRONG&gt; · &lt;STRONG&gt;&lt;A href="https://learn.microsoft.com/en-us/azure/container-apps/express-faq" target="_blank" rel="noopener"&gt;Browse the FAQ&lt;/A&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;When speed matters, ACA Express is the best tool for deploying containers. It skips the platform setup delays without sacrificing reliability under load.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;</description>
      <pubDate>Tue, 30 Jun 2026 20:10:51 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/azure-container-apps-express-for-shipping-container-apps-fast/ba-p/4531371</guid>
      <dc:creator>hetvip</dc:creator>
      <dc:date>2026-06-30T20:10:51Z</dc:date>
    </item>
    <item>
      <title>A Better Way to View Logs in Kudu for Azure App Service on Linux</title>
      <link>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/a-better-way-to-view-logs-in-kudu-for-azure-app-service-on-linux/ba-p/4530934</link>
      <description>&lt;P&gt;Logs are often the fastest way to understand what is happening inside your application. Whether you are investigating startup behavior, runtime errors, failed requests, dependency issues, or unexpected application behavior, having the right log view can make troubleshooting much easier.&lt;/P&gt;
&lt;P&gt;To make this easier, we have added a new&amp;nbsp;&lt;STRONG&gt;Log stream&lt;/STRONG&gt;&amp;nbsp;page in Kudu for Azure App Service on Linux, available under the&amp;nbsp;&lt;STRONG&gt;Logs&lt;/STRONG&gt;&amp;nbsp;dropdown. This experience gives you a single place to stream, browse, search, and filter logs so you can understand what is happening in your app faster.&lt;/P&gt;
&lt;H3&gt;Opening the Logs page&lt;/H3&gt;
&lt;P&gt;You can open Kudu from the Azure portal:&lt;/P&gt;
&lt;OL&gt;
&lt;LI&gt;Go to your&amp;nbsp;&lt;STRONG&gt;App Service&lt;/STRONG&gt;.&lt;/LI&gt;
&lt;LI&gt;Select&amp;nbsp;&lt;STRONG&gt;Advanced Tools&lt;/STRONG&gt;.&lt;/LI&gt;
&lt;LI&gt;Click&amp;nbsp;&lt;STRONG&gt;Go&lt;/STRONG&gt;.&lt;/LI&gt;
&lt;/OL&gt;
&lt;P&gt;You can also open Kudu directly by going to: &lt;EM&gt;https://&amp;lt;app-name&amp;gt;.scm.azurewebsites.net&lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;From there, open the&amp;nbsp;&lt;STRONG&gt;Logs&lt;/STRONG&gt;&amp;nbsp;page.&lt;/P&gt;
&lt;H3&gt;View live logs across your app and platform&lt;/H3&gt;
&lt;P&gt;The Logs page lets you view logs as they are being written, with filters for&amp;nbsp;&lt;STRONG&gt;timeframe&lt;/STRONG&gt;,&amp;nbsp;&lt;STRONG&gt;instance&lt;/STRONG&gt;,&amp;nbsp;&lt;STRONG&gt;container&lt;/STRONG&gt;,&amp;nbsp;&lt;STRONG&gt;log type&lt;/STRONG&gt;, and&amp;nbsp;&lt;STRONG&gt;level&lt;/STRONG&gt;.&lt;/P&gt;
&lt;P&gt;This helps when you want to focus on a specific instance, look only at errors, or separate application logs from platform events.&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;For example, you can use platform logs to understand container lifecycle events, restarts, startup behavior, warmup probe activity, and other platform-side events related to your app.&lt;/P&gt;
&lt;H3&gt;Quickly find the log entries that matter&lt;/H3&gt;
&lt;P&gt;You can use keyword search to narrow down the log stream or historical logs. This is useful when you are looking for a specific error message, request path, exception, dependency failure, timeout, or any application-specific keyword.&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;Instead of scanning through hundreds of entries, you can search for the terms that are relevant to the issue you are investigating.&lt;/P&gt;
&lt;H3&gt;Investigate issues within a specific timeframe&lt;/H3&gt;
&lt;P&gt;The Log stream page also supports viewing logs for a selected time range. This is useful when you know when an issue occurred and want to inspect both application and platform activity around that time.&lt;/P&gt;
&lt;P&gt;For example, you can filter to a specific timeframe, switch to&amp;nbsp;&lt;STRONG&gt;Application&lt;/STRONG&gt; logs, and check what your app was doing when the issue happened.&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;This can help you troubleshoot scenarios such as failed requests, application exceptions, slow startup, container restarts, dependency issues, or configuration problems.&lt;/P&gt;
&lt;H3&gt;Summary&lt;/H3&gt;
&lt;P&gt;The new Log stream page in Kudu makes it easier to work with logs for Azure App Service on Linux. With live streaming, keyword search, historical views, and filters for application and platform logs, you can quickly narrow down the information you need and troubleshoot issues more efficiently.&lt;/P&gt;
&lt;P&gt;We are continuing to improve the App Service Linux experience to make diagnostics simpler and more useful for day-to-day development and operations.&lt;/P&gt;
&lt;P&gt;&lt;BR /&gt;&lt;BR /&gt;&lt;/P&gt;</description>
      <pubDate>Thu, 25 Jun 2026 09:46:14 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/a-better-way-to-view-logs-in-kudu-for-azure-app-service-on-linux/ba-p/4530934</guid>
      <dc:creator>TulikaC</dc:creator>
      <dc:date>2026-06-25T09:46:14Z</dc:date>
    </item>
    <item>
      <title>IPv6 Dual-Stack Endpoints for Azure Container Registry (Public Preview)</title>
      <link>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/ipv6-dual-stack-endpoints-for-azure-container-registry-public/ba-p/4528422</link>
      <description>&lt;P&gt;By &lt;A class="lia-external-url" href="https://www.linkedin.com/in/johnsonshi/" target="_blank" rel="noopener"&gt;Johnson Shi&lt;/A&gt;, &lt;A class="lia-external-url" href="https://www.linkedin.com/in/aviraltakkar/" target="_blank" rel="noopener"&gt;Aviral Takkar&lt;/A&gt;, &lt;A class="lia-external-url" href="https://www.linkedin.com/in/northtyphoon/" target="_blank" rel="noopener"&gt;Bin Du&lt;/A&gt;&lt;!-- fill in or remove --&gt;&lt;/P&gt;
&lt;H2 id="introduction"&gt;Introduction&lt;/H2&gt;
&lt;P&gt;Two of the most common networking questions we hear from teams running Azure Container Registry (ACR) are:&lt;/P&gt;
&lt;OL&gt;
&lt;LI&gt;&lt;STRONG&gt;"Can my registry serve clients on IPv6 networks?"&lt;/STRONG&gt; — Teams operating IPv6-only or dual-stack networks need their container registry reachable over IPv6.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;"How do we start moving registry traffic toward IPv6 without breaking anything?"&lt;/STRONG&gt; — Organizations guarding against IPv4 address exhaustion, or operating under IPv6 transition mandates, want a migration path that doesn't disrupt existing IPv4 clients.&lt;/LI&gt;
&lt;/OL&gt;
&lt;P&gt;Today, we're announcing the&amp;nbsp;&lt;STRONG&gt;public preview of IPv6 dual-stack endpoints&lt;/STRONG&gt; for Azure Container Registry for public endpoints and firewall rules, with IPv6 over private endpoints planned for GA. Set your registry's endpoint protocol to&amp;nbsp;&lt;CODE&gt;IPv4AndIPv6&lt;/CODE&gt;, and its endpoints become reachable over both IPv4 and IPv6 — so IPv4-only, dual-stack, and IPv6-capable clients all connect to the same registry, each over whichever protocol their network stack selects.&lt;/P&gt;
&lt;H2 id="key-takeaways"&gt;Key Takeaways&lt;/H2&gt;
&lt;UL&gt;
&lt;LI&gt;ACR registries now support an &lt;CODE&gt;endpointProtocol&lt;/CODE&gt; setting with two values: &lt;CODE&gt;IPv4&lt;/CODE&gt; (default) and &lt;CODE&gt;IPv4AndIPv6&lt;/CODE&gt; (dual stack, preview).&lt;/LI&gt;
&lt;LI&gt;Dual stack is additive — your registry continues serving IPv4 clients exactly as before. There is no IPv6-only mode.&lt;/LI&gt;
&lt;LI&gt;Dual stack requires &lt;A href="https://learn.microsoft.com/azure/container-registry/container-registry-dedicated-data-endpoints" target="_blank" rel="noopener"&gt;dedicated data endpoints&lt;/A&gt; to be enabled (&lt;CODE&gt;--data-endpoint-enabled true&lt;/CODE&gt;), and dedicated data endpoints require the &lt;STRONG&gt;Premium&lt;/STRONG&gt; SKU. The service enforces this requirement.&lt;/LI&gt;
&lt;LI&gt;You can enable it today with &lt;STRONG&gt;Azure CLI 2.87.0&lt;/STRONG&gt; via &lt;CODE&gt;az acr update --endpoint-protocol IPv4AndIPv6&lt;/CODE&gt;.&lt;/LI&gt;
&lt;LI&gt;FQDN-based client firewall rules keep working unchanged; IP-based allowlists need to account for IPv6 traffic.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Limitation:&amp;nbsp;&lt;/STRONG&gt;This public preview covers IPv6 for the registry's &lt;STRONG&gt;public endpoints&lt;/STRONG&gt; and &lt;STRONG&gt;firewall rules&lt;/STRONG&gt; only. IPv6 over &lt;STRONG&gt;private endpoints&lt;/STRONG&gt; is planned for a future release.&lt;/LI&gt;
&lt;LI&gt;
&lt;P&gt;&lt;STRONG&gt;Limitation&lt;/STRONG&gt;: ACR Tasks isn't supported on a registry that has IPv6 dual-stack enabled. Tasks does not work when the endpoint protocol isIPv6 dual-stack, including quick builds (with az acr build) and quick task runs (with az acr run). Support is planned for a future release.&lt;/P&gt;
&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2 id="how-to-enable-it"&gt;How to enable it&lt;/H2&gt;
&lt;H3 id="on-an-existing-registry-azure-cli-2-87-0-or-later-"&gt;On an existing registry (Azure CLI 2.87.0 or later)&lt;/H3&gt;
&lt;P&gt;Dual stack requires dedicated data endpoints, so enable both in a single update:&lt;/P&gt;
&lt;LI-CODE lang="bash"&gt;az acr update --name &amp;lt;your-registry&amp;gt; --data-endpoint-enabled true --endpoint-protocol IPv4AndIPv6
&lt;/LI-CODE&gt;
&lt;P&gt;If dedicated data endpoints are already enabled, set the endpoint protocol on its own:&lt;/P&gt;
&lt;LI-CODE lang="bash"&gt;az acr update --name &amp;lt;your-registry&amp;gt; --endpoint-protocol IPv4AndIPv6
&lt;/LI-CODE&gt;
&lt;P&gt;Verify the configuration:&lt;/P&gt;
&lt;LI-CODE lang="bash"&gt;az acr show --name &amp;lt;your-registry&amp;gt; --query "{endpointProtocol:endpointProtocol, dataEndpointEnabled:dataEndpointEnabled}"
&lt;/LI-CODE&gt;&lt;LI-CODE lang="json"&gt;{
  "dataEndpointEnabled": true,
  "endpointProtocol": "IPv4AndIPv6"
}
&lt;/LI-CODE&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;STRONG&gt;Note:&lt;/STRONG&gt; If your clients sit behind a firewall and you're enabling dedicated data endpoints for the first time, add firewall rules for &lt;CODE&gt;&amp;lt;your-registry&amp;gt;.&amp;lt;region&amp;gt;.data.azurecr.io&lt;/CODE&gt; &lt;STRONG&gt;before&lt;/STRONG&gt; enabling — switching from &lt;CODE&gt;*.blob.core.windows.net&lt;/CODE&gt; to dedicated data endpoints changes where layer blobs are downloaded from. See &lt;A href="https://learn.microsoft.com/azure/container-registry/container-registry-dedicated-data-endpoints" target="_blank" rel="noopener"&gt;Dedicated data endpoints&lt;/A&gt; for details.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;H3 id="reverting-to-ipv4"&gt;Reverting to IPv4&lt;/H3&gt;
&lt;P&gt;Dual stack is reversible at any time:&lt;/P&gt;
&lt;LI-CODE lang="bash"&gt;az acr update --name &amp;lt;your-registry&amp;gt; --endpoint-protocol IPv4
&lt;/LI-CODE&gt;
&lt;P&gt;Reverting the endpoint protocol leaves dedicated data endpoints enabled; disable them separately if desired.&lt;/P&gt;
&lt;H2 id="scope-of-this-preview"&gt;Scope of this preview&lt;/H2&gt;
&lt;P&gt;This public preview enables IPv6 for the registry's &lt;STRONG&gt;public endpoints&lt;/STRONG&gt; — the login server, dedicated data endpoints, and regional endpoints (if enabled). IPv6 over &lt;STRONG&gt;private endpoints&lt;/STRONG&gt; isn't part of this preview. Support is planned for a future release. Until then, registries reached through a private endpoint continue to use IPv4.&lt;/P&gt;
&lt;P&gt;Additionally, IPv6 dual-stack support for ACR Tasks, including support for `az acr build` and `az acr run`, are not supported in the public preview. Support is planned for a future release.&lt;/P&gt;
&lt;H2 id="requirements-and-how-features-compose"&gt;Requirements and how features compose&lt;/H2&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;&lt;table border="1" style="border-width: 1px;"&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Requirement&lt;/th&gt;&lt;th&gt;Why&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Premium SKU&lt;/td&gt;&lt;td&gt;Dedicated data endpoints are a Premium feature.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Dedicated data endpoints enabled&lt;/td&gt;&lt;td&gt;&lt;CODE&gt;IPv4AndIPv6&lt;/CODE&gt; requires &lt;CODE&gt;dataEndpointEnabled: true&lt;/CODE&gt;; the service rejects the setting otherwise.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Azure CLI 2.87.0+&lt;/td&gt;&lt;td&gt;Adds &lt;CODE&gt;--endpoint-protocol&lt;/CODE&gt; to &lt;CODE&gt;az acr update&lt;/CODE&gt;.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;colgroup&gt;&lt;col style="width: 50.00%" /&gt;&lt;col style="width: 50.00%" /&gt;&lt;/colgroup&gt;&lt;/table&gt;&lt;/DIV&gt;
&lt;P&gt;For geo-replicated registries, the endpoint protocol is a registry-level setting, and dedicated data endpoints exist in every replica region. Firewall guidance: rules based on registry FQDNs — the login server, dedicated data endpoints, and regional endpoints (if enabled) — continue to work unchanged for dual-stack registries; only IP-address-based allowlists need updating for IPv6.&lt;/P&gt;
&lt;P&gt;To learn more, see &lt;A href="https://learn.microsoft.com/azure/container-registry/container-registry-ipv6-dual-stack" target="_blank" rel="noopener"&gt;IPv6 dual-stack endpoints in Azure Container Registry (preview)&lt;/A&gt; &lt;!-- update link once docs PR publishes --&gt; and the &lt;A href="https://learn.microsoft.com/azure/container-registry/container-registry-endpoint-reference" target="_blank" rel="noopener"&gt;ACR endpoint reference&lt;/A&gt;.&lt;/P&gt;
&lt;P&gt;If you have further questions about IPv6 dual-stack endpoints or dedicated data endpoints, reach out to us on the &lt;A href="https://github.com/Azure/acr" target="_blank" rel="noopener"&gt;Azure Container Registry GitHub repository&lt;/A&gt; or file feedback through the Azure portal.&lt;/P&gt;</description>
      <pubDate>Wed, 24 Jun 2026 22:28:46 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/ipv6-dual-stack-endpoints-for-azure-container-registry-public/ba-p/4528422</guid>
      <dc:creator>johnsonshi_msft</dc:creator>
      <dc:date>2026-06-24T22:28:46Z</dc:date>
    </item>
    <item>
      <title>Only 8.5% of MCP Servers Use OAuth — Here's How to Host One Securely on App Service</title>
      <link>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/only-8-5-of-mcp-servers-use-oauth-here-s-how-to-host-one/ba-p/4530349</link>
      <description>&lt;P&gt;The Model Context Protocol exploded onto the scene because it's &lt;EM&gt;easy&lt;/EM&gt;. Stand up a server, expose a few tools, point Claude or VS Code at it, and your agent can suddenly read files, hit APIs, and run code. That same ease is the problem: most MCP servers ship with &lt;STRONG&gt;no authentication at all&lt;/STRONG&gt;, and they're getting pushed straight to the internet.&lt;/P&gt;
&lt;P&gt;The numbers are bleak-into-an-incident-report bad. Astrix Research's &lt;A class="lia-external-url" href="https://astrix.security/learn/blog/state-of-mcp-server-security-2025/" target="_blank"&gt;State of MCP Server Security 2025&lt;/A&gt; found that only &lt;STRONG&gt;8.5%&lt;/STRONG&gt; of MCP servers use OAuth — the rest lean on static API keys or nothing. And the CVEs have already started:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;A class="lia-external-url" href="https://github.com/advisories/GHSA-6xpm-ggf7-wc3p" target="_blank"&gt;CVE-2025-6514&lt;/A&gt;&lt;/STRONG&gt; — a &lt;STRONG&gt;CVSS 9.6&lt;/STRONG&gt; OS command-injection flaw in &lt;CODE&gt;mcp-remote&lt;/CODE&gt;. If a client connects to a malicious or hijacked MCP server, the server can inject shell commands through the OAuth &lt;CODE&gt;authorization_endpoint&lt;/CODE&gt; during discovery and achieve &lt;STRONG&gt;remote code execution on the client&lt;/STRONG&gt;. Roughly half a million downloads were exposed.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;A class="lia-external-url" href="https://nvd.nist.gov/vuln/detail/CVE-2025-49596" target="_blank"&gt;CVE-2025-49596&lt;/A&gt;&lt;/STRONG&gt; — RCE in the MCP &lt;STRONG&gt;Inspector&lt;/STRONG&gt; dev tool, which shipped with no authentication on its local web UI. A crafted request from a webpage you happened to visit could execute code on your machine.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;The throughline: &lt;STRONG&gt;MCP doesn't enforce security at the protocol level.&lt;/STRONG&gt; The &lt;A class="lia-external-url" href="https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization" target="_blank"&gt;spec is explicit&lt;/A&gt; that authorization is optional and implementation-dependent. That's a reasonable design choice for a transport, but it means &lt;EM&gt;you&lt;/EM&gt; own the perimeter. Skip it, and you've published an unauthenticated RPC endpoint that can read secrets and run tools.&lt;/P&gt;
&lt;P&gt;So let's not skip it. This post walks through a &lt;STRONG&gt;hardened MCP server on Azure App Service&lt;/STRONG&gt; that closes every gap above — and most of it is platform configuration, not code you have to write and get right yourself.&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;STRONG&gt;Sample:&lt;/STRONG&gt; &lt;A class="lia-external-url" href="https://github.com/seligj95/app-service-secure-mcp" target="_blank"&gt;seligj95/app-service-secure-mcp&lt;/A&gt;. One &lt;CODE&gt;azd up&lt;/CODE&gt; (plus an Entra app registration the hook creates for you) gives you an MCP server behind Easy Auth, talking to Key Vault over a managed identity, with no public network access, fronted by API Management, and an Application Insights alert watching for abuse.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;H3&gt;The threat model for a hosted MCP server&lt;/H3&gt;
&lt;P&gt;Before the architecture, be honest about the attack surface. When an MCP server is internet-reachable, the bad days look like this:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Unauthenticated tool invocation.&lt;/STRONG&gt; Anyone who finds the endpoint calls your tools. If one of them reads a database or a secret, that's the whole game.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Credential exfiltration.&lt;/STRONG&gt; A tool that returns a secret value — even "helpfully," for debugging — hands credentials to whatever is driving the session.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Prompt injection via tool responses.&lt;/STRONG&gt; A compromised or malicious tool return can carry instructions that hijack the calling agent.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Path traversal / injection.&lt;/STRONG&gt; A tool that concatenates user input into a file path or shell command is the same class of bug we've fought for 25 years, now with an LLM cheerfully supplying the payload.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Lateral movement.&lt;/STRONG&gt; A server running with a broad identity or a network line of sight to everything becomes a pivot point.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;The architecture below maps a defense to each one. None of it is exotic — it's the App Service security stack, pointed at MCP.&lt;/P&gt;
&lt;H3&gt;The architecture&lt;/H3&gt;
&lt;PRE&gt;&amp;nbsp;&lt;/PRE&gt;
&lt;img /&gt;
&lt;PRE&gt;&amp;nbsp;&lt;/PRE&gt;
&lt;P&gt;Five layers, each one a checkbox or a few lines of Bicep. Let's take them in order.&lt;/P&gt;
&lt;H3&gt;1. Easy Auth — spec-compliant OAuth you don't have to write&lt;/H3&gt;
&lt;P&gt;The single most important fix is also the easiest: turn on App Service &lt;STRONG&gt;built-in authentication&lt;/STRONG&gt; (Easy Auth) and point it at Entra ID. Now App Service validates the token and rejects unauthenticated requests &lt;STRONG&gt;at the platform&lt;/STRONG&gt;, before a single line of your Python runs.&lt;/P&gt;
&lt;P&gt;App Service Authentication also has a &lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/app-service/configure-authentication-mcp" target="_blank"&gt;&lt;STRONG&gt;built-in MCP server authorization mode (Preview)&lt;/STRONG&gt;&lt;/A&gt; that makes your server comply with the &lt;A class="lia-external-url" href="https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization" target="_blank"&gt;MCP authorization spec&lt;/A&gt;: it serves &lt;STRONG&gt;Protected Resource Metadata (PRM)&lt;/STRONG&gt; so a compliant MCP client can discover the authorization server and complete the OAuth handshake itself — instead of just getting a bare 401.&lt;/P&gt;
&lt;P&gt;In the sample that's an &lt;CODE&gt;authsettingsV2&lt;/CODE&gt; resource:&lt;/P&gt;
&lt;PRE class="language-bicep" tabindex="0"&gt;&lt;CODE class="language-bicep"&gt;&lt;SPAN class="token keyword"&gt;resource&lt;/SPAN&gt; authSettings &lt;SPAN class="token string"&gt;'Microsoft.Web/sites/config@2024-04-01'&lt;/SPAN&gt; &lt;SPAN class="token operator"&gt;=&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;{&lt;/SPAN&gt;
  &lt;SPAN class="token property"&gt;parent&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; web
  &lt;SPAN class="token property"&gt;name&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token string"&gt;'authsettingsV2'&lt;/SPAN&gt;
  &lt;SPAN class="token property"&gt;properties&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;{&lt;/SPAN&gt;
    &lt;SPAN class="token property"&gt;globalValidation&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;{&lt;/SPAN&gt;
      &lt;SPAN class="token property"&gt;requireAuthentication&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token boolean"&gt;true&lt;/SPAN&gt;
      &lt;SPAN class="token property"&gt;unauthenticatedClientAction&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token string"&gt;'Return401'&lt;/SPAN&gt;   &lt;SPAN class="token comment"&gt;// reject, don't redirect&lt;/SPAN&gt;
    &lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;
    &lt;SPAN class="token property"&gt;identityProviders&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;{&lt;/SPAN&gt;
      &lt;SPAN class="token property"&gt;azureActiveDirectory&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;{&lt;/SPAN&gt;
        &lt;SPAN class="token property"&gt;enabled&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token boolean"&gt;true&lt;/SPAN&gt;
        &lt;SPAN class="token property"&gt;registration&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;{&lt;/SPAN&gt;
          &lt;SPAN class="token property"&gt;clientId&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; authClientId
          &lt;SPAN class="token property"&gt;openIdIssuer&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token interpolated-string"&gt;&lt;SPAN class="token string"&gt;'&lt;/SPAN&gt;&lt;SPAN class="token interpolation"&gt;&lt;SPAN class="token punctuation"&gt;${&lt;/SPAN&gt;&lt;SPAN class="token expression"&gt;&lt;SPAN class="token function"&gt;environment&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;(&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;)&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;.&lt;/SPAN&gt;authentication&lt;SPAN class="token punctuation"&gt;.&lt;/SPAN&gt;loginEndpoint&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN class="token interpolation"&gt;&lt;SPAN class="token punctuation"&gt;${&lt;/SPAN&gt;&lt;SPAN class="token expression"&gt;authTenantId&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN class="token string"&gt;/v2.0'&lt;/SPAN&gt;&lt;/SPAN&gt;
        &lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;
        &lt;SPAN class="token property"&gt;validation&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;{&lt;/SPAN&gt;
          &lt;SPAN class="token property"&gt;allowedAudiences&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;[&lt;/SPAN&gt; &lt;SPAN class="token interpolated-string"&gt;&lt;SPAN class="token string"&gt;'api://&lt;/SPAN&gt;&lt;SPAN class="token interpolation"&gt;&lt;SPAN class="token punctuation"&gt;${&lt;/SPAN&gt;&lt;SPAN class="token expression"&gt;authClientId&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN class="token string"&gt;'&lt;/SPAN&gt;&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;]&lt;/SPAN&gt;
        &lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;
      &lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;
    &lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;
  &lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;
&lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;The piece that makes it MCP-&lt;EM&gt;compliant&lt;/EM&gt; — not just "returns 401" — is enabling PRM. That's one app setting that publishes the metadata document MCP clients look for:&lt;/P&gt;
&lt;PRE class="language-bicep" tabindex="0"&gt;&lt;CODE class="language-bicep"&gt;&lt;SPAN class="token punctuation"&gt;{&lt;/SPAN&gt;
  &lt;SPAN class="token property"&gt;name&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token string"&gt;'WEBSITE_AUTH_PRM_DEFAULT_WITH_SCOPES'&lt;/SPAN&gt;
  &lt;SPAN class="token property"&gt;value&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token interpolated-string"&gt;&lt;SPAN class="token string"&gt;'api://&lt;/SPAN&gt;&lt;SPAN class="token interpolation"&gt;&lt;SPAN class="token punctuation"&gt;${&lt;/SPAN&gt;&lt;SPAN class="token expression"&gt;authClientId&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN class="token string"&gt;/user_impersonation'&lt;/SPAN&gt;&lt;/SPAN&gt;
&lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;&lt;CODE&gt;unauthenticatedClientAction: 'Return401'&lt;/CODE&gt; gives a clean 401 instead of a login redirect, and PRM turns that 401 into a &lt;EM&gt;discoverable&lt;/EM&gt; OAuth challenge — the client follows the metadata, signs the user in, and retries with a valid token. Recall that &lt;STRONG&gt;8.5%&lt;/STRONG&gt; figure: this is the spec-compliant OAuth the other 91.5% are missing, and you got it from configuration, not code.&lt;/P&gt;
&lt;P&gt;One gotcha worth calling out: when App Service creates the Entra registration for you, the default policy only accepts tokens the &lt;STRONG&gt;app itself&lt;/STRONG&gt; obtained. For a real MCP client to connect, add its client id to the allowed-applications policy and &lt;STRONG&gt;preauthorize&lt;/STRONG&gt; it on the app registration. (Entra has no Dynamic Client Registration, so the client ships a known client id; for VS Code / GitHub Copilot, preauthorization avoids a consent prompt the client won't surface.)&lt;/P&gt;
&lt;P&gt;The bonus is that the validated identity is &lt;EM&gt;handed to your code&lt;/EM&gt;. App Service injects the caller's claims into every forwarded request as the &lt;CODE&gt;X-MS-CLIENT-PRINCIPAL&lt;/CODE&gt; headers — and crucially, it &lt;STRONG&gt;strips any client-supplied copy first&lt;/STRONG&gt;, so they can't be forged. The &lt;CODE&gt;whoami&lt;/CODE&gt; tool just reads them:&lt;/P&gt;
&lt;PRE class="language-python" tabindex="0"&gt;&lt;CODE class="language-python"&gt;&lt;SPAN class="token keyword"&gt;def&lt;/SPAN&gt; &lt;SPAN class="token function"&gt;_client_principal&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;(&lt;/SPAN&gt;request&lt;SPAN class="token punctuation"&gt;:&lt;/SPAN&gt; Request&lt;SPAN class="token punctuation"&gt;)&lt;/SPAN&gt; &lt;SPAN class="token operator"&gt;-&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;&amp;gt;&lt;/SPAN&gt; Dict&lt;SPAN class="token punctuation"&gt;[&lt;/SPAN&gt;&lt;SPAN class="token builtin"&gt;str&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;,&lt;/SPAN&gt; Any&lt;SPAN class="token punctuation"&gt;]&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;:&lt;/SPAN&gt;
    raw &lt;SPAN class="token operator"&gt;=&lt;/SPAN&gt; request&lt;SPAN class="token punctuation"&gt;.&lt;/SPAN&gt;headers&lt;SPAN class="token punctuation"&gt;.&lt;/SPAN&gt;get&lt;SPAN class="token punctuation"&gt;(&lt;/SPAN&gt;&lt;SPAN class="token string"&gt;"x-ms-client-principal"&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;)&lt;/SPAN&gt;
    &lt;SPAN class="token comment"&gt;# base64-encoded JSON of the caller's claims, injected by Easy Auth&lt;/SPAN&gt;
    &lt;SPAN class="token punctuation"&gt;.&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;.&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;.&lt;/SPAN&gt;
    &lt;SPAN class="token keyword"&gt;return&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;{&lt;/SPAN&gt;&lt;SPAN class="token string"&gt;"authenticated"&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token builtin"&gt;bool&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;(&lt;/SPAN&gt;raw&lt;SPAN class="token punctuation"&gt;)&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;,&lt;/SPAN&gt; &lt;SPAN class="token string"&gt;"name"&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;:&lt;/SPAN&gt; name&lt;SPAN class="token punctuation"&gt;,&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;.&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;.&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;.&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;Your tools now &lt;EM&gt;know who's calling&lt;/EM&gt; without you owning any of the token machinery.&lt;/P&gt;
&lt;H3&gt;2. Managed identity — stop storing the keys to the kingdom&lt;/H3&gt;
&lt;P&gt;The static-API-key habit is how secrets leak. Replace it with a &lt;STRONG&gt;system-assigned managed identity&lt;/STRONG&gt;: App Service gets an Entra identity that Azure manages, and your code authenticates to Key Vault, Storage, or Azure OpenAI with &lt;STRONG&gt;no stored credential&lt;/STRONG&gt;.&lt;/P&gt;
&lt;P&gt;This matters for a subtle reason the MCP authorization guidance calls out explicitly: the token a client presents represents access to &lt;EM&gt;your server&lt;/EM&gt;, not to Key Vault. &lt;STRONG&gt;Never forward it downstream&lt;/STRONG&gt; — use the managed identity (or an on-behalf-of token) for that hop. Pass-through is a vulnerability; delegation is the fix, and the managed identity is how you delegate without holding a secret.&lt;/P&gt;
&lt;PRE class="language-bicep" tabindex="0"&gt;&lt;CODE class="language-bicep"&gt;&lt;SPAN class="token keyword"&gt;resource&lt;/SPAN&gt; web &lt;SPAN class="token string"&gt;'Microsoft.Web/sites@2024-04-01'&lt;/SPAN&gt; &lt;SPAN class="token operator"&gt;=&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;{&lt;/SPAN&gt;
  &lt;SPAN class="token property"&gt;identity&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;{&lt;/SPAN&gt; type&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token string"&gt;'SystemAssigned'&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;
  &lt;SPAN class="token operator"&gt;...&lt;/SPAN&gt;
&lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;In Python, &lt;CODE&gt;DefaultAzureCredential&lt;/CODE&gt; resolves to that identity automatically — the same code runs locally against your &lt;CODE&gt;az login&lt;/CODE&gt; and in Azure against the MI:&lt;/P&gt;
&lt;PRE class="language-python" tabindex="0"&gt;&lt;CODE class="language-python"&gt;&lt;SPAN class="token keyword"&gt;from&lt;/SPAN&gt; azure&lt;SPAN class="token punctuation"&gt;.&lt;/SPAN&gt;identity &lt;SPAN class="token keyword"&gt;import&lt;/SPAN&gt; DefaultAzureCredential
&lt;SPAN class="token keyword"&gt;from&lt;/SPAN&gt; azure&lt;SPAN class="token punctuation"&gt;.&lt;/SPAN&gt;keyvault&lt;SPAN class="token punctuation"&gt;.&lt;/SPAN&gt;secrets &lt;SPAN class="token keyword"&gt;import&lt;/SPAN&gt; SecretClient

credential &lt;SPAN class="token operator"&gt;=&lt;/SPAN&gt; DefaultAzureCredential&lt;SPAN class="token punctuation"&gt;(&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;)&lt;/SPAN&gt;
client &lt;SPAN class="token operator"&gt;=&lt;/SPAN&gt; SecretClient&lt;SPAN class="token punctuation"&gt;(&lt;/SPAN&gt;vault_url&lt;SPAN class="token operator"&gt;=&lt;/SPAN&gt;KEY_VAULT_URI&lt;SPAN class="token punctuation"&gt;,&lt;/SPAN&gt; credential&lt;SPAN class="token operator"&gt;=&lt;/SPAN&gt;credential&lt;SPAN class="token punctuation"&gt;)&lt;/SPAN&gt;
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;And least privilege is one role assignment. The sample grants the identity exactly &lt;CODE&gt;Key Vault Secrets User&lt;/CODE&gt; — read secret values, nothing else:&lt;/P&gt;
&lt;PRE class="language-bicep" tabindex="0"&gt;&lt;CODE class="language-bicep"&gt;&lt;SPAN class="token keyword"&gt;var&lt;/SPAN&gt; keyVaultSecretsUserRoleId &lt;SPAN class="token operator"&gt;=&lt;/SPAN&gt; &lt;SPAN class="token string"&gt;'4633458b-17de-408a-b874-0445c86b69e6'&lt;/SPAN&gt;
&lt;SPAN class="token keyword"&gt;resource&lt;/SPAN&gt; appSecretsUser &lt;SPAN class="token string"&gt;'Microsoft.Authorization/roleAssignments@2022-04-01'&lt;/SPAN&gt; &lt;SPAN class="token operator"&gt;=&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;{&lt;/SPAN&gt;
  &lt;SPAN class="token property"&gt;name&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token function"&gt;guid&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;(&lt;/SPAN&gt;keyVault&lt;SPAN class="token punctuation"&gt;.&lt;/SPAN&gt;id&lt;SPAN class="token punctuation"&gt;,&lt;/SPAN&gt; appPrincipalId&lt;SPAN class="token punctuation"&gt;,&lt;/SPAN&gt; keyVaultSecretsUserRoleId&lt;SPAN class="token punctuation"&gt;)&lt;/SPAN&gt;
  &lt;SPAN class="token property"&gt;scope&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; keyVault
  &lt;SPAN class="token property"&gt;properties&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;{&lt;/SPAN&gt;
    &lt;SPAN class="token property"&gt;roleDefinitionId&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token function"&gt;subscriptionResourceId&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;(&lt;/SPAN&gt;&lt;SPAN class="token string"&gt;'Microsoft.Authorization/roleDefinitions'&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;,&lt;/SPAN&gt; keyVaultSecretsUserRoleId&lt;SPAN class="token punctuation"&gt;)&lt;/SPAN&gt;
    &lt;SPAN class="token property"&gt;principalId&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; appPrincipalId
    &lt;SPAN class="token property"&gt;principalType&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token string"&gt;'ServicePrincipal'&lt;/SPAN&gt;
  &lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;
&lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;There is now no secret used to read secrets. That's the chain of custody you want.&lt;/P&gt;
&lt;H3&gt;3. Key Vault references — and a tool that &lt;EM&gt;won't&lt;/EM&gt; leak them&lt;/H3&gt;
&lt;P&gt;Two pieces here. First, &lt;STRONG&gt;Key Vault references&lt;/STRONG&gt; keep secrets out of your configuration. You point an app setting at a vault URI, and App Service resolves the value at runtime via the managed identity:&lt;/P&gt;
&lt;PRE class="language-bicep" tabindex="0"&gt;&lt;CODE class="language-bicep"&gt;&lt;SPAN class="token punctuation"&gt;{&lt;/SPAN&gt;
  &lt;SPAN class="token property"&gt;name&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token string"&gt;'SECURE_CONFIG_VALUE'&lt;/SPAN&gt;
  &lt;SPAN class="token property"&gt;value&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token interpolated-string"&gt;&lt;SPAN class="token string"&gt;'@Microsoft.KeyVault(SecretUri=&lt;/SPAN&gt;&lt;SPAN class="token interpolation"&gt;&lt;SPAN class="token punctuation"&gt;${&lt;/SPAN&gt;&lt;SPAN class="token expression"&gt;secureConfigSecretUri&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN class="token string"&gt;)'&lt;/SPAN&gt;&lt;/SPAN&gt;
&lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;The plaintext never appears in your repo, your Bicep, or the portal's app settings blade — it shows up as a resolved reference.&lt;/P&gt;
&lt;P&gt;Second, and this is the part developers get wrong: &lt;STRONG&gt;a tool that reads a secret should never return the secret.&lt;/STRONG&gt; The sample's &lt;CODE&gt;read_secret_metadata&lt;/CODE&gt; proves the managed-identity path works end to end, then deliberately withholds the value:&lt;/P&gt;
&lt;PRE class="language-python" tabindex="0"&gt;&lt;CODE class="language-python"&gt;&lt;SPAN class="token keyword"&gt;async&lt;/SPAN&gt; &lt;SPAN class="token keyword"&gt;def&lt;/SPAN&gt; &lt;SPAN class="token function"&gt;tool_read_secret_metadata&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;(&lt;/SPAN&gt;secret_name&lt;SPAN class="token punctuation"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token builtin"&gt;str&lt;/SPAN&gt; &lt;SPAN class="token operator"&gt;=&lt;/SPAN&gt; &lt;SPAN class="token string"&gt;"demo-secret"&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;)&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;:&lt;/SPAN&gt;
    secret &lt;SPAN class="token operator"&gt;=&lt;/SPAN&gt; client&lt;SPAN class="token punctuation"&gt;.&lt;/SPAN&gt;get_secret&lt;SPAN class="token punctuation"&gt;(&lt;/SPAN&gt;secret_name&lt;SPAN class="token punctuation"&gt;)&lt;/SPAN&gt;
    &lt;SPAN class="token keyword"&gt;return&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;{&lt;/SPAN&gt;
        &lt;SPAN class="token string"&gt;"available"&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token boolean"&gt;True&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;,&lt;/SPAN&gt;
        &lt;SPAN class="token string"&gt;"version"&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;:&lt;/SPAN&gt; secret&lt;SPAN class="token punctuation"&gt;.&lt;/SPAN&gt;properties&lt;SPAN class="token punctuation"&gt;.&lt;/SPAN&gt;version&lt;SPAN class="token punctuation"&gt;,&lt;/SPAN&gt;
        &lt;SPAN class="token string"&gt;"value_length"&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token builtin"&gt;len&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;(&lt;/SPAN&gt;secret&lt;SPAN class="token punctuation"&gt;.&lt;/SPAN&gt;value&lt;SPAN class="token punctuation"&gt;)&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;,&lt;/SPAN&gt;   &lt;SPAN class="token comment"&gt;# length, never the value&lt;/SPAN&gt;
        &lt;SPAN class="token string"&gt;"note"&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token string"&gt;"Value intentionally withheld — metadata only."&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;,&lt;/SPAN&gt;
    &lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;If your MCP server has a &lt;CODE&gt;get_secret&lt;/CODE&gt; tool that returns the secret, you've built a credential-exfiltration API with a friendly name. Return metadata; act on the value server-side.&lt;/P&gt;
&lt;P&gt;The same discipline applies to &lt;EM&gt;input&lt;/EM&gt;. The &lt;CODE&gt;safe_lookup&lt;/CODE&gt; tool matches against a fixed allow-list and refuses anything that smells like traversal or injection — it never touches a filesystem or a shell:&lt;/P&gt;
&lt;PRE class="language-python" tabindex="0"&gt;&lt;CODE class="language-python"&gt;suspicious &lt;SPAN class="token operator"&gt;=&lt;/SPAN&gt; &lt;SPAN class="token builtin"&gt;any&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;(&lt;/SPAN&gt;t &lt;SPAN class="token keyword"&gt;in&lt;/SPAN&gt; key &lt;SPAN class="token keyword"&gt;for&lt;/SPAN&gt; t &lt;SPAN class="token keyword"&gt;in&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;(&lt;/SPAN&gt;&lt;SPAN class="token string"&gt;".."&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;,&lt;/SPAN&gt; &lt;SPAN class="token string"&gt;"/"&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;,&lt;/SPAN&gt; &lt;SPAN class="token string"&gt;"\\"&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;,&lt;/SPAN&gt; &lt;SPAN class="token string"&gt;";"&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;,&lt;/SPAN&gt; &lt;SPAN class="token string"&gt;"|"&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;,&lt;/SPAN&gt; &lt;SPAN class="token string"&gt;"$("&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;,&lt;/SPAN&gt; &lt;SPAN class="token string"&gt;"`"&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;)&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;)&lt;/SPAN&gt;
&lt;SPAN class="token keyword"&gt;if&lt;/SPAN&gt; key &lt;SPAN class="token keyword"&gt;in&lt;/SPAN&gt; DOCS&lt;SPAN class="token punctuation"&gt;:&lt;/SPAN&gt;
    &lt;SPAN class="token keyword"&gt;return&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;{&lt;/SPAN&gt;&lt;SPAN class="token string"&gt;"topic"&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;:&lt;/SPAN&gt; key&lt;SPAN class="token punctuation"&gt;,&lt;/SPAN&gt; &lt;SPAN class="token string"&gt;"doc"&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;:&lt;/SPAN&gt; DOCS&lt;SPAN class="token punctuation"&gt;[&lt;/SPAN&gt;key&lt;SPAN class="token punctuation"&gt;]&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;,&lt;/SPAN&gt; &lt;SPAN class="token string"&gt;"found"&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token boolean"&gt;True&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;
&lt;SPAN class="token keyword"&gt;return&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;{&lt;/SPAN&gt;&lt;SPAN class="token string"&gt;"found"&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token boolean"&gt;False&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;,&lt;/SPAN&gt; &lt;SPAN class="token string"&gt;"rejected_as_suspicious"&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;:&lt;/SPAN&gt; suspicious&lt;SPAN class="token punctuation"&gt;,&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;.&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;.&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;.&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;&lt;CODE&gt;safe_lookup("../../etc/passwd")&lt;/CODE&gt; comes back &lt;CODE&gt;rejected_as_suspicious: true&lt;/CODE&gt;. That is the entire fix for a whole class of CVEs.&lt;/P&gt;
&lt;H3&gt;4. Private endpoints + APIM — take the server off the internet&lt;/H3&gt;
&lt;P&gt;Authentication is necessary but not sufficient. The strongest version of "don't expose your MCP server" is to &lt;STRONG&gt;not expose it&lt;/STRONG&gt; — give the App Service and Key Vault private endpoints, disable public network access, and let &lt;STRONG&gt;API Management&lt;/STRONG&gt; be the only public door.&lt;/P&gt;
&lt;PRE class="language-bicep" tabindex="0"&gt;&lt;CODE class="language-bicep"&gt;&lt;SPAN class="token keyword"&gt;resource&lt;/SPAN&gt; web &lt;SPAN class="token string"&gt;'Microsoft.Web/sites@2024-04-01'&lt;/SPAN&gt; &lt;SPAN class="token operator"&gt;=&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;{&lt;/SPAN&gt;
  &lt;SPAN class="token property"&gt;properties&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;{&lt;/SPAN&gt;
    &lt;SPAN class="token property"&gt;virtualNetworkSubnetId&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; appSubnetId     &lt;SPAN class="token comment"&gt;// outbound: reach KV's private endpoint&lt;/SPAN&gt;
    &lt;SPAN class="token property"&gt;publicNetworkAccess&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token string"&gt;'Disabled'&lt;/SPAN&gt;         &lt;SPAN class="token comment"&gt;// inbound: no public access at all&lt;/SPAN&gt;
    &lt;SPAN class="token operator"&gt;...&lt;/SPAN&gt;
  &lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;
&lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;Now the App Service hostname returns nothing from the internet. The only ingress is the APIM gateway, which runs the security policy &lt;EM&gt;before&lt;/EM&gt; traffic ever reaches the VNet — validate the Entra JWT, rate-limit per caller, and (the documented extension point) run a content-safety check:&lt;/P&gt;
&lt;PRE class="language-xml" tabindex="0"&gt;&lt;CODE class="language-xml"&gt;&lt;SPAN class="token tag"&gt;&lt;SPAN class="token tag"&gt;&lt;SPAN class="token punctuation"&gt;&amp;lt;&lt;/SPAN&gt;inbound&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;&amp;gt;&lt;/SPAN&gt;&lt;/SPAN&gt;
  &lt;SPAN class="token tag"&gt;&lt;SPAN class="token tag"&gt;&lt;SPAN class="token punctuation"&gt;&amp;lt;&lt;/SPAN&gt;base&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;/&amp;gt;&lt;/SPAN&gt;&lt;/SPAN&gt;
  &lt;SPAN class="token tag"&gt;&lt;SPAN class="token tag"&gt;&lt;SPAN class="token punctuation"&gt;&amp;lt;&lt;/SPAN&gt;validate-jwt&lt;/SPAN&gt; &lt;SPAN class="token attr-name"&gt;header-name&lt;/SPAN&gt;&lt;SPAN class="token attr-value"&gt;&lt;SPAN class="token punctuation attr-equals"&gt;=&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;"&lt;/SPAN&gt;Authorization&lt;SPAN class="token punctuation"&gt;"&lt;/SPAN&gt;&lt;/SPAN&gt; &lt;SPAN class="token attr-name"&gt;failed-validation-httpcode&lt;/SPAN&gt;&lt;SPAN class="token attr-value"&gt;&lt;SPAN class="token punctuation attr-equals"&gt;=&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;"&lt;/SPAN&gt;401&lt;SPAN class="token punctuation"&gt;"&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;&amp;gt;&lt;/SPAN&gt;&lt;/SPAN&gt;
    &lt;SPAN class="token tag"&gt;&lt;SPAN class="token tag"&gt;&lt;SPAN class="token punctuation"&gt;&amp;lt;&lt;/SPAN&gt;openid-config&lt;/SPAN&gt; &lt;SPAN class="token attr-name"&gt;url&lt;/SPAN&gt;&lt;SPAN class="token attr-value"&gt;&lt;SPAN class="token punctuation attr-equals"&gt;=&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;"&lt;/SPAN&gt;https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration&lt;SPAN class="token punctuation"&gt;"&lt;/SPAN&gt;&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;/&amp;gt;&lt;/SPAN&gt;&lt;/SPAN&gt;
    &lt;SPAN class="token tag"&gt;&lt;SPAN class="token tag"&gt;&lt;SPAN class="token punctuation"&gt;&amp;lt;&lt;/SPAN&gt;required-claims&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;&amp;gt;&lt;/SPAN&gt;&lt;/SPAN&gt;
      &lt;SPAN class="token tag"&gt;&lt;SPAN class="token tag"&gt;&lt;SPAN class="token punctuation"&gt;&amp;lt;&lt;/SPAN&gt;claim&lt;/SPAN&gt; &lt;SPAN class="token attr-name"&gt;name&lt;/SPAN&gt;&lt;SPAN class="token attr-value"&gt;&lt;SPAN class="token punctuation attr-equals"&gt;=&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;"&lt;/SPAN&gt;aud&lt;SPAN class="token punctuation"&gt;"&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;&amp;gt;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN class="token tag"&gt;&lt;SPAN class="token tag"&gt;&lt;SPAN class="token punctuation"&gt;&amp;lt;&lt;/SPAN&gt;value&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;&amp;gt;&lt;/SPAN&gt;&lt;/SPAN&gt;api://{clientId}&lt;SPAN class="token tag"&gt;&lt;SPAN class="token tag"&gt;&lt;SPAN class="token punctuation"&gt;&amp;lt;/&lt;/SPAN&gt;value&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;&amp;gt;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN class="token tag"&gt;&lt;SPAN class="token tag"&gt;&lt;SPAN class="token punctuation"&gt;&amp;lt;/&lt;/SPAN&gt;claim&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;&amp;gt;&lt;/SPAN&gt;&lt;/SPAN&gt;
    &lt;SPAN class="token tag"&gt;&lt;SPAN class="token tag"&gt;&lt;SPAN class="token punctuation"&gt;&amp;lt;/&lt;/SPAN&gt;required-claims&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;&amp;gt;&lt;/SPAN&gt;&lt;/SPAN&gt;
  &lt;SPAN class="token tag"&gt;&lt;SPAN class="token tag"&gt;&lt;SPAN class="token punctuation"&gt;&amp;lt;/&lt;/SPAN&gt;validate-jwt&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;&amp;gt;&lt;/SPAN&gt;&lt;/SPAN&gt;
  &lt;SPAN class="token tag"&gt;&lt;SPAN class="token tag"&gt;&lt;SPAN class="token punctuation"&gt;&amp;lt;&lt;/SPAN&gt;rate-limit-by-key&lt;/SPAN&gt; &lt;SPAN class="token attr-name"&gt;calls&lt;/SPAN&gt;&lt;SPAN class="token attr-value"&gt;&lt;SPAN class="token punctuation attr-equals"&gt;=&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;"&lt;/SPAN&gt;60&lt;SPAN class="token punctuation"&gt;"&lt;/SPAN&gt;&lt;/SPAN&gt; &lt;SPAN class="token attr-name"&gt;renewal-period&lt;/SPAN&gt;&lt;SPAN class="token attr-value"&gt;&lt;SPAN class="token punctuation attr-equals"&gt;=&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;"&lt;/SPAN&gt;60&lt;SPAN class="token punctuation"&gt;"&lt;/SPAN&gt;&lt;/SPAN&gt;
                     &lt;SPAN class="token attr-name"&gt;counter-key&lt;/SPAN&gt;&lt;SPAN class="token attr-value"&gt;&lt;SPAN class="token punctuation attr-equals"&gt;=&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;"&lt;/SPAN&gt;@(context.Request.IpAddress)&lt;SPAN class="token punctuation"&gt;"&lt;/SPAN&gt;&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;/&amp;gt;&lt;/SPAN&gt;&lt;/SPAN&gt;
  &lt;SPAN class="token tag"&gt;&lt;SPAN class="token tag"&gt;&lt;SPAN class="token punctuation"&gt;&amp;lt;&lt;/SPAN&gt;set-backend-service&lt;/SPAN&gt; &lt;SPAN class="token attr-name"&gt;backend-id&lt;/SPAN&gt;&lt;SPAN class="token attr-value"&gt;&lt;SPAN class="token punctuation attr-equals"&gt;=&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;"&lt;/SPAN&gt;mcp-backend&lt;SPAN class="token punctuation"&gt;"&lt;/SPAN&gt;&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;/&amp;gt;&lt;/SPAN&gt;&lt;/SPAN&gt;
&lt;SPAN class="token tag"&gt;&lt;SPAN class="token tag"&gt;&lt;SPAN class="token punctuation"&gt;&amp;lt;/&lt;/SPAN&gt;inbound&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;&amp;gt;&lt;/SPAN&gt;&lt;/SPAN&gt;
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;This is defense in depth: APIM validates the token, &lt;EM&gt;and&lt;/EM&gt; Easy Auth validates it again at the app. An attacker has to get past a public gateway with JWT enforcement and rate limiting just to reach a private endpoint that &lt;EM&gt;also&lt;/EM&gt; demands a valid token. Compare that to the median MCP server, which is a raw port on the internet.&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;STRONG&gt;The honest trade-off:&lt;/STRONG&gt; this is a security reference architecture, not a 60-second demo. APIM takes ~30–45 minutes to provision, and because the app is private, you test &lt;EM&gt;through the gateway&lt;/EM&gt;, not the App Service hostname. That friction is the point — it's the same friction an attacker hits.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;H3&gt;5. Monitoring — see the abuse before it's an incident&lt;/H3&gt;
&lt;P&gt;The last layer is visibility. The Azure Monitor OpenTelemetry distro auto-instruments FastAPI, and the &lt;CODE&gt;audit_event&lt;/CODE&gt; tool emits a structured custom event per call. A scheduled-query alert watches the rate of those events and fires when tool invocations spike — the signature of an agent looping over a sensitive tool, or someone probing the surface:&lt;/P&gt;
&lt;PRE class="language-bicep" tabindex="0"&gt;&lt;CODE class="language-bicep"&gt;criteria&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;{&lt;/SPAN&gt;
  &lt;SPAN class="token property"&gt;allOf&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;[&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;{&lt;/SPAN&gt;
    &lt;SPAN class="token property"&gt;query&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token string"&gt;'customEvents | where name == "mcp_tool_audit" | summarize calls = count() by bin(timestamp, 5m)'&lt;/SPAN&gt;
    &lt;SPAN class="token property"&gt;metricMeasureColumn&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token string"&gt;'calls'&lt;/SPAN&gt;
    &lt;SPAN class="token property"&gt;operator&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token string"&gt;'GreaterThan'&lt;/SPAN&gt;
    &lt;SPAN class="token property"&gt;threshold&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;:&lt;/SPAN&gt; &lt;SPAN class="token number"&gt;100&lt;/SPAN&gt;
  &lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;&lt;SPAN class="token punctuation"&gt;]&lt;/SPAN&gt;
&lt;SPAN class="token punctuation"&gt;}&lt;/SPAN&gt;
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;Tune the threshold to your baseline. The point is that "is someone hammering my credential-reading tool?" becomes an alert, not a forensic exercise after the fact.&lt;/P&gt;
&lt;H3&gt;Deploy it&lt;/H3&gt;
&lt;PRE class="language-bash" tabindex="0"&gt;&lt;CODE class="language-bash"&gt;azd auth login
azd up
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;A preprovision hook creates the Entra ID app registration and stashes its client id in the azd environment, so Easy Auth and the APIM policy wire themselves up. Then Bicep provisions the VNet, private endpoints, Key Vault, App Service, APIM, and the monitoring stack. (Grab a coffee for the APIM step.)&lt;/P&gt;
&lt;P&gt;To verify, get a token and call through the gateway:&lt;/P&gt;
&lt;PRE class="language-bash" tabindex="0"&gt;&lt;CODE class="language-bash"&gt;&lt;SPAN class="token assign-left variable"&gt;TOKEN&lt;/SPAN&gt;&lt;SPAN class="token operator"&gt;=&lt;/SPAN&gt;&lt;SPAN class="token variable"&gt;&lt;SPAN class="token variable"&gt;$(&lt;/SPAN&gt;az account get-access-token &lt;SPAN class="token punctuation"&gt;\&lt;/SPAN&gt;
  --resource &lt;SPAN class="token string"&gt;"api://&lt;SPAN class="token variable"&gt;&lt;SPAN class="token variable"&gt;$(&lt;/SPAN&gt;azd &lt;SPAN class="token function"&gt;env&lt;/SPAN&gt; get-value AZURE_AUTH_CLIENT_ID&lt;SPAN class="token variable"&gt;)&lt;/SPAN&gt;&lt;/SPAN&gt;"&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;\&lt;/SPAN&gt;
  --query accessToken -o tsv&lt;SPAN class="token variable"&gt;)&lt;/SPAN&gt;&lt;/SPAN&gt;

&lt;SPAN class="token function"&gt;curl&lt;/SPAN&gt; -s -X POST &lt;SPAN class="token string"&gt;"&lt;SPAN class="token variable"&gt;&lt;SPAN class="token variable"&gt;$(&lt;/SPAN&gt;azd &lt;SPAN class="token function"&gt;env&lt;/SPAN&gt; get-value APIM_MCP_URL&lt;SPAN class="token variable"&gt;)&lt;/SPAN&gt;&lt;/SPAN&gt;"&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;\&lt;/SPAN&gt;
  -H &lt;SPAN class="token string"&gt;"Authorization: Bearer &lt;SPAN class="token variable"&gt;$TOKEN&lt;/SPAN&gt;"&lt;/SPAN&gt; -H &lt;SPAN class="token string"&gt;'content-type: application/json'&lt;/SPAN&gt; &lt;SPAN class="token punctuation"&gt;\&lt;/SPAN&gt;
  -d &lt;SPAN class="token string"&gt;'{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"whoami","arguments":{}}}'&lt;/SPAN&gt;
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;The response shows the authenticated principal. Drop the token and you get a &lt;CODE&gt;401&lt;/CODE&gt; from APIM — exactly what you want an unauthenticated caller to see.&lt;/P&gt;
&lt;P&gt;To let a real MCP client (VS Code, Claude) sign users in itself rather than pasting a bearer token, point it at the same URL: PRM is already published, so the client discovers the auth server and runs the OAuth flow. Just make sure its app id is allowed — &lt;CODE&gt;azd env set AZURE_MCP_CLIENT_APP_ID &amp;lt;client-id&amp;gt;&lt;/CODE&gt; before &lt;CODE&gt;azd up&lt;/CODE&gt; adds it to the allowed-applications policy — and preauthorize it on the server's app registration so clients that don't surface a consent prompt can connect.&lt;/P&gt;
&lt;P&gt;Once it's deployed and you've verified it, take the App Service off the public internet with a one-line flip — &lt;CODE&gt;azd env set LOCK_DOWN_WEB_APP true &amp;amp;&amp;amp; azd
provision&lt;/CODE&gt;. (The first deploy keeps public access on just long enough to push the code, because a fully-private app can only be deployed from inside its VNet. The sample's README walks through both phases.)&lt;/P&gt;
&lt;H3&gt;Why this matters&lt;/H3&gt;
&lt;P&gt;MCP is going to be the USB-C of agent tooling, and right now most of the connectors are unauthenticated and exposed. The CVEs aren't hypothetical — they have numbers and CVSS scores. But the fix isn't a research project. On App Service, the perimeter is mostly configuration: flip on Easy Auth, use a managed identity, reference Key Vault, go private, front it with APIM, and alert on the logs.&lt;/P&gt;
&lt;P&gt;That's the difference between "I shipped an MCP server" and "I shipped an MCP server I'd put in production." If you're hosting MCP — especially anywhere a compliance auditor will eventually look — start from the secure shape, not the demo shape.&lt;/P&gt;
&lt;H3&gt;Try it&lt;/H3&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Sample repo:&lt;/STRONG&gt; &lt;A class="lia-external-url" href="https://github.com/seligj95/app-service-secure-mcp" target="_blank"&gt;github.com/seligj95/app-service-secure-mcp&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Astrix — State of MCP Server Security 2025:&lt;/STRONG&gt; &lt;A class="lia-external-url" href="https://astrix.security/learn/blog/state-of-mcp-server-security-2025/" target="_blank"&gt;astrix.security&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;MCP authorization spec:&lt;/STRONG&gt; &lt;A class="lia-external-url" href="https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization" target="_blank"&gt;modelcontextprotocol.io&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;App Service authentication:&lt;/STRONG&gt; &lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/app-service/configure-authentication-mcp" target="_blank"&gt;learn.microsoft.com&lt;/A&gt;&lt;/LI&gt;
&lt;/UL&gt;</description>
      <pubDate>Tue, 23 Jun 2026 19:20:54 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/only-8-5-of-mcp-servers-use-oauth-here-s-how-to-host-one/ba-p/4530349</guid>
      <dc:creator>jordanselig</dc:creator>
      <dc:date>2026-06-23T19:20:54Z</dc:date>
    </item>
    <item>
      <title>MCP Just Went Stateless — What the 2026 Spec Changes About Scaling on App Service</title>
      <link>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/mcp-just-went-stateless-what-the-2026-spec-changes-about-scaling/ba-p/4530222</link>
      <description>&lt;P&gt;A couple of months ago I wrote about&amp;nbsp;&lt;A class="lia-internal-link lia-internal-url lia-internal-url-content-type-blog" href="https://techcommunity.microsoft.com/blog/appsonazureblog/you-can-scale-mcp-servers-behind-a-load-balancer-on-app-service-%E2%80%94-heres-how/4520824" target="_blank" rel="noopener" data-lia-auto-title="scaling MCP servers behind App Service's built-in load balancer" data-lia-auto-title-active="0"&gt;scaling MCP servers behind App Service's built-in load balancer&lt;/A&gt;. The trick back then was to lean on &lt;STRONG&gt;stateless HTTP transport&lt;/STRONG&gt; so any instance could serve any request — and to make sure you turned off ARR affinity so the load balancer was actually free to spread traffic around.&lt;/P&gt;
&lt;P&gt;That post still works. But the MCP spec just caught up to it in a big way.&lt;/P&gt;
&lt;P&gt;The &lt;STRONG&gt;&lt;CODE&gt;2026-07-28&lt;/CODE&gt; release candidate&lt;/STRONG&gt; is the largest revision of the Model Context Protocol since it launched, and the headline change is exactly the thing we were working around: &lt;STRONG&gt;MCP is now stateless at the protocol layer.&lt;/STRONG&gt; The handshake is gone, the session header is gone, and the sticky-routing-and-shared-session-store dance that horizontal deployments used to need is no longer part of the protocol at all.&lt;/P&gt;
&lt;P&gt;If you're hosting an MCP server on App Service, this is good news — and it means a few of the steps from my last post are now things the protocol does for you. Here's what changed, and what (if anything) you need to do about it.&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;Here's the before and after, straight from the spec. In &lt;CODE&gt;2025-11-25&lt;/CODE&gt;, the client &lt;CODE&gt;POST&lt;/CODE&gt;s an &lt;CODE&gt;initialize&lt;/CODE&gt; call to &lt;CODE&gt;/mcp&lt;/CODE&gt; first and gets a session ID back:&lt;/P&gt;
&lt;PRE&gt;&lt;CODE&gt;{"jsonrpc":"2.0","id":1,"method":"initialize",
 "params":{"protocolVersion":"2025-11-25","capabilities":{},
 "clientInfo":{"name":"my-app","version":"1.0"}}}
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;&lt;STRONG&gt;Heads up on timing:&lt;/STRONG&gt; &lt;CODE&gt;2026-07-28&lt;/CODE&gt; is a release candidate as I write this; the final spec ships &lt;STRONG&gt;July 28, 2026&lt;/STRONG&gt;. It contains breaking changes, so treat this as "get ready" guidance rather than "rip everything out today."&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;H2&gt;Quick recap: how we scaled MCP before&lt;/H2&gt;
&lt;P&gt;In the original post, the recipe looked like this:&lt;/P&gt;
&lt;OL&gt;
&lt;LI&gt;Run the MCP server in &lt;STRONG&gt;stateless HTTP&lt;/STRONG&gt; mode (the &lt;CODE&gt;2025-11-25&lt;/CODE&gt; transport).&lt;/LI&gt;
&lt;LI&gt;Scale App Service out to N instances (the sample used &lt;STRONG&gt;three&lt;/STRONG&gt;).&lt;/LI&gt;
&lt;LI&gt;Set &lt;STRONG&gt;&lt;CODE&gt;clientAffinityEnabled: false&lt;/CODE&gt;&lt;/STRONG&gt; so there's no ARR affinity cookie pinning a client to one instance.&lt;/LI&gt;
&lt;LI&gt;If you genuinely needed cross-request state, externalize it — typically into &lt;STRONG&gt;Azure Cache for Redis&lt;/STRONG&gt; — so every instance saw the same data.&lt;/LI&gt;
&lt;LI&gt;Watch traffic spread across instances in Application Insights via &lt;CODE&gt;cloud_RoleInstance&lt;/CODE&gt;.&lt;/LI&gt;
&lt;/OL&gt;
&lt;P&gt;The catch: even in "stateless HTTP" mode, the &lt;CODE&gt;2025-11-25&lt;/CODE&gt; protocol still started every connection with an &lt;CODE&gt;initialize&lt;/CODE&gt; handshake and handed back an &lt;STRONG&gt;&lt;CODE&gt;Mcp-Session-Id&lt;/CODE&gt;&lt;/STRONG&gt; that the client had to send on every follow-up request. That session ID pinned a client to whichever instance issued it — so to scale cleanly you either kept affinity on (and gave up even load balancing) or did real work to share session state across instances.&lt;/P&gt;
&lt;P&gt;That's the part the 2026 spec deletes.&lt;/P&gt;
&lt;H2&gt;What the 2026 spec actually changes&lt;/H2&gt;
&lt;H3&gt;The handshake and the session are gone&lt;/H3&gt;
&lt;P&gt;Two proposals do the heavy lifting:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;A class="lia-external-url" href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575" target="_blank" rel="noopener"&gt;SEP-2575&lt;/A&gt;&lt;/STRONG&gt; removes the &lt;CODE&gt;initialize&lt;/CODE&gt; / &lt;CODE&gt;initialized&lt;/CODE&gt; handshake. The protocol version, client info, and client capabilities that used to be exchanged once at connect time now ride along in &lt;CODE&gt;_meta&lt;/CODE&gt; on &lt;STRONG&gt;every&lt;/STRONG&gt; request. A new &lt;CODE&gt;server/discover&lt;/CODE&gt; method lets a client ask for server capabilities when it actually wants them.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;A class="lia-external-url" href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2567" target="_blank" rel="noopener"&gt;SEP-2567&lt;/A&gt;&lt;/STRONG&gt; removes the &lt;CODE&gt;Mcp-Session-Id&lt;/CODE&gt; header and the protocol-level session that came with it.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;With both gone, &lt;STRONG&gt;any MCP request can land on any instance.&lt;/STRONG&gt; The sticky routing and shared session stores that horizontal deployments needed before just aren't required at the protocol layer anymore.&lt;/P&gt;
&lt;P&gt;Here's the before and after, straight from the spec. In &lt;CODE&gt;2025-11-25&lt;/CODE&gt;, the client &lt;CODE&gt;POST&lt;/CODE&gt;s an &lt;CODE&gt;initialize&lt;/CODE&gt; call to &lt;CODE&gt;/mcp&lt;/CODE&gt; first and gets a session ID back:&lt;/P&gt;
&lt;LI-CODE lang="json"&gt;{"jsonrpc":"2.0","id":1,"method":"initialize",
 "params":{"protocolVersion":"2025-11-25","capabilities":{},
 "clientInfo":{"name":"my-app","version":"1.0"}}}&lt;/LI-CODE&gt;
&lt;P&gt;…then every later call has to carry the&amp;nbsp;&lt;CODE&gt;Mcp-Session-Id&lt;/CODE&gt; header the server handed back, which pins it to that instance:&lt;/P&gt;
&lt;LI-CODE lang="json"&gt;{"jsonrpc":"2.0","id":2,"method":"tools/call",
 "params":{"name":"search","arguments":{"q":"otters"}}}&lt;/LI-CODE&gt;
&lt;P&gt;In &lt;CODE&gt;2026-07-28&lt;/CODE&gt;, the same tool call is one self-contained request that any instance can answer. The routing info rides in headers — &lt;CODE&gt;MCP-Protocol-Version&lt;/CODE&gt;, &lt;CODE&gt;Mcp-Method&lt;/CODE&gt;, and &lt;CODE&gt;Mcp-Name&lt;/CODE&gt; — and the body carries everything else:&lt;/P&gt;
&lt;LI-CODE lang="json"&gt;{"jsonrpc":"2.0","id":1,"method":"tools/call",
 "params":{"name":"search","arguments":{"q":"otters"},
 "_meta":{"io.modelcontextprotocol/clientInfo":{"name":"my-app","version":"1.0"}}}}&lt;/LI-CODE&gt;
&lt;P&gt;No handshake, no session ID, nothing to pin.&lt;/P&gt;
&lt;H3&gt;Traffic you can route and cache at the edge&lt;/H3&gt;
&lt;P&gt;A few smaller changes make this traffic much friendlier to the infrastructure App Service already gives you:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Routable headers (&lt;A class="lia-external-url" href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243" target="_blank" rel="noopener"&gt;SEP-2243&lt;/A&gt;):&lt;/STRONG&gt; Streamable HTTP now requires &lt;CODE&gt;Mcp-Method&lt;/CODE&gt; and &lt;CODE&gt;Mcp-Name&lt;/CODE&gt; headers, so load balancers, gateways, and rate-limiters can route or throttle on the &lt;EM&gt;operation&lt;/EM&gt; without cracking open the request body. (Servers reject requests where the headers and body disagree.)&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Cacheable lists (&lt;A class="lia-external-url" href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549" target="_blank" rel="noopener"&gt;SEP-2549&lt;/A&gt;):&lt;/STRONG&gt; &lt;CODE&gt;tools/list&lt;/CODE&gt; and resource-read results now carry &lt;CODE&gt;ttlMs&lt;/CODE&gt; and &lt;CODE&gt;cacheScope&lt;/CODE&gt;, modeled on HTTP &lt;CODE&gt;Cache-Control&lt;/CODE&gt;. Clients know exactly how long a tool list is fresh and whether it's safe to share across users — no more holding an SSE stream open just to learn the list changed.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Traceable calls (&lt;A class="lia-external-url" href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/414" target="_blank" rel="noopener"&gt;SEP-414&lt;/A&gt;):&lt;/STRONG&gt; W3C Trace Context (&lt;CODE&gt;traceparent&lt;/CODE&gt;, &lt;CODE&gt;tracestate&lt;/CODE&gt;, &lt;CODE&gt;baggage&lt;/CODE&gt;) propagation in &lt;CODE&gt;_meta&lt;/CODE&gt; is now documented with fixed key names. A trace that starts in the host app can follow a tool call through the client SDK, your MCP server, and whatever it calls downstream — and show up as one span tree in any OpenTelemetry backend, including &lt;STRONG&gt;Application Insights&lt;/STRONG&gt;.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;That last one pairs really nicely with the App Insights setup from the original sample, which already tags spans with &lt;CODE&gt;cloud_RoleInstance&lt;/CODE&gt;.&lt;/P&gt;
&lt;H2&gt;Why this is &lt;EM&gt;easier&lt;/EM&gt; on App Service now&lt;/H2&gt;
&lt;P&gt;App Service's built-in load balancer has always wanted to round-robin your requests. The thing stopping it from doing that cleanly with MCP was the protocol's own session affinity. Now that the protocol is stateless:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;No affinity tuning to reason about.&lt;/STRONG&gt; You still want &lt;CODE&gt;clientAffinityEnabled: false&lt;/CODE&gt;, but there's no longer a protocol session fighting it.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Any instance serves any request, for real.&lt;/STRONG&gt; Scale from 3 to 10 instances and the load balancer just spreads the work — no shared session store required for protocol state.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Less Redis glue.&lt;/STRONG&gt; In the old model, Redis was often there to share &lt;EM&gt;protocol&lt;/EM&gt; session state. That reason is gone (see the next section for what Redis is still great for).&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2&gt;"Stateless protocol" doesn't mean "stateless app"&lt;/H2&gt;
&lt;P&gt;This is the part I want to be really clear about, because it's easy to over-read the headline.&lt;/P&gt;
&lt;P&gt;Removing the protocol session &lt;STRONG&gt;does not&lt;/STRONG&gt; mean your application can't have state. It means the &lt;EM&gt;protocol&lt;/EM&gt; stops carrying state for you. If your server needs to remember something across calls, you do what HTTP APIs have always done: &lt;STRONG&gt;mint an explicit handle and let the model pass it back as an argument.&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;The spec calls this the explicit-handle pattern. A tool returns a &lt;CODE&gt;basket_id&lt;/CODE&gt; (or &lt;CODE&gt;browser_id&lt;/CODE&gt;, or whatever), and later calls include that ID as a normal parameter:&lt;/P&gt;
&lt;PRE&gt;&lt;CODE&gt;// 1) create returns a handle
{"name": "create_basket", "arguments": {}}
// -&amp;gt; { "basket_id": "b_12345" }

// 2) later calls pass it back as an ordinary argument
{"name": "add_item", "arguments": {"basket_id": "b_12345", "sku": "ABC"}}
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;The nice side effect: the model can &lt;EM&gt;see&lt;/EM&gt; the handle, compose it across tools, and hand it off between steps — in ways that session state hidden in transport metadata never really allowed.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;So where does Redis fit now?&lt;/STRONG&gt; Exactly where it always belonged — your &lt;EM&gt;application's&lt;/EM&gt; data, not the protocol's plumbing:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Backing store for those explicit handles (what's actually in basket &lt;CODE&gt;b_12345&lt;/CODE&gt;).&lt;/LI&gt;
&lt;LI&gt;Caching expensive lookups or model responses across instances.&lt;/LI&gt;
&lt;LI&gt;App-level conversation memory or rate-limit counters.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Stateless protocol, stateful application. You externalize state because &lt;EM&gt;your app&lt;/EM&gt; needs it shared, not because the transport forces you to.&lt;/P&gt;
&lt;H2&gt;Migrating an existing MCP server on App Service&lt;/H2&gt;
&lt;P&gt;If you deployed the original sample (or something like it), here's the punch list to get to the 2026 model. The good news: the App Service / infra side barely changes — most of the work is in the protocol layer your SDK handles for you.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;App Service config — mostly already done:&lt;/STRONG&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Keep &lt;CODE&gt;clientAffinityEnabled: false&lt;/CODE&gt;. (Still the right call.)&lt;/LI&gt;
&lt;LI&gt;Keep scaling out to N instances. Nothing here changes.&lt;/LI&gt;
&lt;LI&gt;Keep Application Insights + OpenTelemetry — and lean into the new Trace Context key names for cleaner end-to-end traces.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;STRONG&gt;Protocol layer — the real work:&lt;/STRONG&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Update to an SDK build that speaks &lt;CODE&gt;2026-07-28&lt;/CODE&gt;. The handshake and session handling go away; your server reads protocol version and client info from &lt;CODE&gt;_meta&lt;/CODE&gt; per request instead of from an &lt;CODE&gt;initialize&lt;/CODE&gt; exchange.&lt;/LI&gt;
&lt;LI&gt;Emit &lt;CODE&gt;ttlMs&lt;/CODE&gt; / &lt;CODE&gt;cacheScope&lt;/CODE&gt; on &lt;CODE&gt;tools/list&lt;/CODE&gt; and resource reads so clients (and your gateway) can cache them.&lt;/LI&gt;
&lt;LI&gt;Make sure your server honors / validates the &lt;CODE&gt;Mcp-Method&lt;/CODE&gt; and &lt;CODE&gt;Mcp-Name&lt;/CODE&gt; headers.&lt;/LI&gt;
&lt;LI&gt;If you were storing anything keyed off &lt;CODE&gt;Mcp-Session-Id&lt;/CODE&gt;, move it to the explicit-handle pattern (handle in, handle out, state in Redis/Cosmos/etc.).&lt;/LI&gt;
&lt;LI&gt;Audit for the breaking bits: &lt;CODE&gt;tasks/list&lt;/CODE&gt; is removed, Roots/Sampling/Logging are deprecated, and the "resource not found" error code moves from &lt;CODE&gt;-32002&lt;/CODE&gt; to the standard &lt;CODE&gt;-32602&lt;/CODE&gt;.&lt;/LI&gt;
&lt;/UL&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;I built a standalone companion sample for exactly this — the &lt;CODE&gt;2026-07-28&lt;/CODE&gt; version of the original, with the handshake gone, everything read from &lt;CODE&gt;_meta&lt;/CODE&gt;, &lt;CODE&gt;server/discover&lt;/CODE&gt; implemented, and the explicit-handle pattern shown in a real tool. Link below.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;H2&gt;Try it yourself&lt;/H2&gt;
&lt;P&gt;I built a companion sample for this post: a FastAPI MCP server that speaks &lt;CODE&gt;2026-07-28&lt;/CODE&gt; natively — no handshake, no session — running on three App Service instances behind the built-in load balancer, with a staging slot, App Insights, a spec-compliant client, and a k6 load test:&lt;/P&gt;
&lt;P&gt;👉 &lt;STRONG&gt;&lt;A class="lia-external-url" href="https://github.com/seligj95/app-service-mcp-stateless-scale-2026-python" target="_blank" rel="noopener"&gt;seligj95/app-service-mcp-stateless-scale-2026-python&lt;/A&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;PRE&gt;&lt;CODE&gt;azd auth login
azd up
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;That provisions a Premium v3 plan with &lt;STRONG&gt;&lt;CODE&gt;capacity: 3&lt;/CODE&gt;&lt;/STRONG&gt;, the web app with &lt;STRONG&gt;&lt;CODE&gt;clientAffinityEnabled: false&lt;/CODE&gt;&lt;/STRONG&gt;, a staging slot, and Log Analytics + Application Insights. No &lt;CODE&gt;initialize&lt;/CODE&gt;, no &lt;CODE&gt;Mcp-Session-Id&lt;/CODE&gt; anywhere — discovery is a single &lt;CODE&gt;server/discover&lt;/CODE&gt; call, and every request carries its own protocol version and client info in &lt;CODE&gt;_meta&lt;/CODE&gt;.&lt;/P&gt;
&lt;P&gt;The part I like best is the &lt;CODE&gt;tally&lt;/CODE&gt; tool. It keeps a running total across calls using an explicit, signed &lt;STRONG&gt;handle&lt;/STRONG&gt; instead of a session — so you can watch the total stay correct even as the load balancer routes each call to a different instance:&lt;/P&gt;
&lt;PRE&gt;&lt;CODE&gt;+10  -&amp;gt; total=10   served_by=2103650c...
+5   -&amp;gt; total=15   served_by=08fc7022...   (different instance, total still right)
+100 -&amp;gt; total=115  served_by=08fc7022...
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;That's the stateless handle pattern from earlier, made concrete: state travels with the request, not the connection. Then watch the load spread in Application Insights:&lt;/P&gt;
&lt;PRE&gt;&lt;CODE&gt;requests
| where timestamp &amp;gt; ago(15m)
| where name contains "/mcp"
| summarize count() by cloud_RoleInstance
&lt;/CODE&gt;&lt;/PRE&gt;
&lt;P&gt;Want the &lt;CODE&gt;2025-11-25&lt;/CODE&gt; version for comparison? That's the original Part 1 sample: &lt;STRONG&gt;&lt;A class="lia-external-url" href="https://github.com/seligj95/app-service-mcp-stateless-scale-python" target="_blank" rel="noopener"&gt;seligj95/app-service-mcp-stateless-scale-python&lt;/A&gt;&lt;/STRONG&gt;. Diff the two &lt;CODE&gt;main.py&lt;/CODE&gt; files and you can see the handshake and session handling simply disappear.&lt;/P&gt;
&lt;H2&gt;The takeaway&lt;/H2&gt;
&lt;P&gt;When I wrote the first post, "make MCP stateless so App Service can load-balance it" was a &lt;EM&gt;pattern you had to apply&lt;/EM&gt;. With the 2026 spec, it's just &lt;STRONG&gt;how MCP works&lt;/STRONG&gt;. The protocol deleted the exact friction we were routing around — which means hosting a horizontally scaled MCP server on App Service is now closer to "deploy a normal web app and scale it out" than ever.&lt;/P&gt;
&lt;P&gt;If you're already running MCP on App Service: you did the hard part early. The spec just made it official.&lt;/P&gt;
&lt;HR /&gt;
&lt;P&gt;&lt;EM&gt;Got an MCP server running on App Service? I'd love to hear how the migration goes — drop a comment.&lt;/EM&gt;&lt;/P&gt;</description>
      <pubDate>Tue, 23 Jun 2026 16:14:36 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/mcp-just-went-stateless-what-the-2026-spec-changes-about-scaling/ba-p/4530222</guid>
      <dc:creator>jordanselig</dc:creator>
      <dc:date>2026-06-23T16:14:36Z</dc:date>
    </item>
    <item>
      <title>How Many Copies of Each Layer Does Your Container Registry Actually Need?</title>
      <link>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/how-many-copies-of-each-layer-does-your-container-registry/ba-p/4529836</link>
      <description>&lt;P data-line="4"&gt;&lt;STRONG&gt;Authors:&lt;/STRONG&gt;&amp;nbsp;&lt;A href="https://www.linkedin.com/in/payalmahesh/" target="_blank"&gt;Payal Mahesh&lt;/A&gt; and &lt;A href="https://www.linkedin.com/in/zeyuanlin/" target="_blank"&gt;Vicky Lin&lt;/A&gt;&lt;/P&gt;
&lt;P data-line="6"&gt;&lt;STRONG&gt;Azure Container Registry team:&lt;/STRONG&gt; &lt;A href="https://www.linkedin.com/in/jeanine-v-burke-89a669a9/" target="_blank"&gt;Jeanine Burke&lt;/A&gt; and &lt;A href="https://www.linkedin.com/in/johnsonshi/" target="_blank"&gt;Johnson Shi&lt;/A&gt;&lt;/P&gt;
&lt;H2 data-line="10"&gt;Introduction&lt;/H2&gt;
&lt;P data-line="12"&gt;It's Monday morning. You spin up a fresh 1,000-node AKS cluster for a big training run or a fleet-wide rollout. Every node reaches for the same large container image at the same instant. What actually happens in the next ten minutes - and whether your pods reach&amp;nbsp;&lt;EM&gt;Ready&lt;/EM&gt;&amp;nbsp;in 9 minutes or 14 - turns out to depend on a single number you've probably never thought about:&amp;nbsp;&lt;STRONG&gt;how many copies of each image layer exist behind your registry.&lt;/STRONG&gt;&lt;/P&gt;
&lt;P data-line="14"&gt;At the surface, you see a single capacity number for your registry size - but behind that abstraction, Azure Container Registry maintains copies of your layer data to optimize pull performance. That number of copies directly determines the read throughput available per layer. Each copy can serve requests independently, so distributing the layer across storage allows it to be read in parallel. More copies mean more independent readers - and higher aggregate throughput when thousands of nodes pull at once.&lt;/P&gt;
&lt;P data-line="16"&gt;The intuitive answer is that more is better: add copies, get faster pulls. When we actually tested it at 1,000-node scale, the truth turned out to be more interesting:&lt;/P&gt;
&lt;UL data-line="18"&gt;
&lt;LI data-line="18"&gt;A few extra copies helped a little.&lt;/LI&gt;
&lt;LI data-line="19"&gt;A moderate number helped a lot, and eliminated storage throttling entirely.&lt;/LI&gt;
&lt;LI data-line="20"&gt;A large number helped no more than the moderate one.&lt;/LI&gt;
&lt;LI data-line="21"&gt;A&amp;nbsp;&lt;EM&gt;huge&lt;/EM&gt;&amp;nbsp;number actually made pulls&amp;nbsp;&lt;STRONG&gt;slower&lt;/STRONG&gt;&amp;nbsp;again.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P data-line="23"&gt;Think of it like opening checkout lanes at a grocery store. Opening a few more lanes when the store is slammed cuts the line dramatically. Past a certain point, though, extra lanes barely help, because by then it's the customers, not the cashiers, who are the bottleneck. And open too many? Now the staff is spread thin and tripping over each other, and the line moves worse than it did at the sweet spot.&lt;/P&gt;
&lt;P data-line="25"&gt;This post walks through what we measured, why the curve bends where it does, and what we're building next so finding that sweet spot isn't something anyone has to do by hand.&lt;/P&gt;
&lt;H2 data-line="27"&gt;Key Takeaways&lt;/H2&gt;
&lt;UL data-line="29"&gt;
&lt;LI data-line="29"&gt;&lt;STRONG&gt;There's a sweet spot, not a slope.&lt;/STRONG&gt;&amp;nbsp;Adding copies per layer cut pod-startup P99 by 27% and raised P50 per-node egress throughput by 244%, but only up to a point. Past that, the returns vanish, and far past it, latency actually regresses.&lt;/LI&gt;
&lt;LI data-line="30"&gt;&lt;STRONG&gt;Storage throttling is the real enemy.&lt;/STRONG&gt;&amp;nbsp;The win comes from spreading load across enough storage backends that no single backend gets pinned at its egress ceiling. Once throttling is gone, more copies stop helping.&lt;/LI&gt;
&lt;LI data-line="31"&gt;&lt;STRONG&gt;Storage scale alone has a ceiling.&lt;/STRONG&gt;&amp;nbsp;Even at the sweet spot, the per-backend egress limit caps total throughput. The next jump in performance has to come from somewhere else, which is exactly what we're building (see&amp;nbsp;&lt;EM&gt;What's Next&lt;/EM&gt;).&lt;/LI&gt;
&lt;LI data-line="32"&gt;&lt;STRONG&gt;This isn't something customers should need to manage.&lt;/STRONG&gt; We're building a proactive, on-demand storage scaling capability that automatically grows the footprint before throttling happens and shrinks it back when the burst is over.&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2 data-line="34"&gt;A quick bit of background&lt;/H2&gt;
&lt;P data-line="36"&gt;Within a region, the layer data behind your container images is backed by Azure storage. The number of copies ACR maintains per layer determines how many independent storage backends a concurrent-pull workload can spread its reads across. That's what matters, because each backend has a finite egress ceiling. Once concurrent reads against one backend get close to that ceiling, requests start getting throttled, and your pulls slow down in proportion.&lt;/P&gt;
&lt;P data-line="38"&gt;The principle is simple: more copies per layer means more backends serving the same data, which means more total egress headroom and fewer throttled requests. What we wanted data on was how many, and where it stops helping.&lt;/P&gt;
&lt;H2 data-line="42"&gt;How we tested&lt;/H2&gt;
&lt;P data-line="44"&gt;We ran a controlled series of large-scale pull tests against ACR Premium on a roughly 1,000-node cluster, with every node pulling the same large image cold at the same time (no local cache on any node). The only thing we changed between runs was the number of per-layer copies behind a single registry endpoint. Everything else, including rate limits, the image, node count, and concurrency, stayed constant.&lt;/P&gt;
&lt;P data-line="46"&gt;For each run we measured pod-startup latency (P50/P90/P99), end-to-end storage read latency, egress throughput distributions (P50-P99.9), and storage throttling events. Pod-startup latency is our headline metric, because it's the one number that reflects the actual customer experience no matter where the bottleneck happens to be. Per-node egress throughput matters too, though. It tells you directly how much pull bandwidth ACR delivers to your fleet, and it's usually what customers have in mind when they ask how much faster extra copies will make their pulls. We report egress as a distribution rather than a single average, since per-request and per-time-window views can tell very different stories about the same set of pulls.&lt;/P&gt;
&lt;P data-line="48"&gt;These are observations from a single controlled environment, not a service guarantee. Absolute numbers will move with image size, node count, layer composition, network topology, and concurrency.&lt;/P&gt;
&lt;H2 data-line="52"&gt;What we found&lt;/H2&gt;
&lt;P data-line="54"&gt;We tested five configurations, sweeping from a low baseline number of per-layer copies up to a very high one. We name them by relative copy count rather than exact instance counts:&lt;/P&gt;
&lt;UL data-line="56"&gt;
&lt;LI data-line="56"&gt;&lt;STRONG&gt;Baseline&lt;/STRONG&gt;: the lowest level, our reference point.&lt;/LI&gt;
&lt;LI data-line="57"&gt;&lt;STRONG&gt;Low&lt;/STRONG&gt;: a modest step up from Baseline.&lt;/LI&gt;
&lt;LI data-line="58"&gt;&lt;STRONG&gt;Mid&lt;/STRONG&gt;: a meaningful step up from Low.&lt;/LI&gt;
&lt;LI data-line="59"&gt;&lt;STRONG&gt;Higher&lt;/STRONG&gt;: a further step up from Mid.&lt;/LI&gt;
&lt;LI data-line="60"&gt;&lt;STRONG&gt;Very high&lt;/STRONG&gt;: the largest configuration we tested, well above Higher.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P data-line="62"&gt;Here are the numbers. All percent changes are relative to Baseline.&lt;/P&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Configuration&lt;/th&gt;&lt;th&gt;Pod startup P50&lt;/th&gt;&lt;th&gt;Pod startup P90&lt;/th&gt;&lt;th&gt;Pod startup P99&lt;/th&gt;&lt;th&gt;Storage throttling events&lt;/th&gt;&lt;th&gt;Peak per-backend egress&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;STRONG&gt;Baseline&lt;/STRONG&gt;&amp;nbsp;(fewest copies)&lt;/td&gt;&lt;td&gt;9m 36s&lt;/td&gt;&lt;td&gt;11m 0s&lt;/td&gt;&lt;td&gt;14m 16s&lt;/td&gt;&lt;td&gt;Many; all top backends above the egress ceiling&lt;/td&gt;&lt;td&gt;Highest&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;STRONG&gt;Low&lt;/STRONG&gt;&lt;/td&gt;&lt;td&gt;9m 27s (−2%)&lt;/td&gt;&lt;td&gt;10m 14s (−7%)&lt;/td&gt;&lt;td&gt;12m 59s (−9%)&lt;/td&gt;&lt;td&gt;Some; one backend still above the ceiling&lt;/td&gt;&lt;td&gt;High&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;STRONG&gt;Mid&lt;/STRONG&gt;&lt;/td&gt;&lt;td&gt;9m 25s (−2%)&lt;/td&gt;&lt;td&gt;9m 45s (−11%)&lt;/td&gt;&lt;td&gt;&lt;STRONG&gt;10m 22s (−27%)&lt;/STRONG&gt;&lt;/td&gt;&lt;td&gt;&lt;STRONG&gt;Zero&lt;/STRONG&gt;&lt;/td&gt;&lt;td&gt;Below the ceiling&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;STRONG&gt;Higher&lt;/STRONG&gt;&lt;/td&gt;&lt;td&gt;9m 20s (−3%)&lt;/td&gt;&lt;td&gt;9m 37s (−13%)&lt;/td&gt;&lt;td&gt;&lt;STRONG&gt;10m 22s (−27%)&lt;/STRONG&gt;&lt;/td&gt;&lt;td&gt;&lt;STRONG&gt;Zero&lt;/STRONG&gt;&lt;/td&gt;&lt;td&gt;Well below the ceiling&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;STRONG&gt;Very high&lt;/STRONG&gt;&lt;/td&gt;&lt;td&gt;9m 28s (−1%)&lt;/td&gt;&lt;td&gt;10m 31s (−4%)&lt;/td&gt;&lt;td&gt;13m 48s (−3%)&lt;/td&gt;&lt;td&gt;&lt;STRONG&gt;Zero&lt;/STRONG&gt;&lt;/td&gt;&lt;td&gt;Lowest&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/DIV&gt;
&lt;P data-line="72"&gt;Look at the P99 pod-startup column from top to bottom: 14m 16s, 12m 59s, 10m 22s, 10m 22s, 13m 48s. It improves, flattens out, then climbs back up. Three things explain that shape:&lt;/P&gt;
&lt;H3 data-line="74"&gt;1. The win: Throttling falls off a cliff at the Mid configuration&lt;/H3&gt;
&lt;P data-line="76"&gt;As we added copies per layer, per-backend egress fell and storage-side throttling decreased. At the Mid configuration, throttling errors hit zero, and they stayed at zero for every configuration above it.&lt;/P&gt;
&lt;P data-line="78"&gt;The upside isn't just that the errors went away, though. It's raw pull bandwidth. At the Mid sweet spot, the typical node saw its P50 egress throughput jump 244% over Baseline. With load spread across enough copies, each node pulled its layers off storage much faster, not just without stalling.&lt;/P&gt;
&lt;P data-line="80"&gt;For a workload owner, that's the difference between watching pods come up in a steady stream and watching them stall for tens of seconds at a time while throttling clears. Same image, same node count, same registry, very different experience.&lt;/P&gt;
&lt;P data-line="82"&gt;To put it in concrete terms: if your team runs a daily AI training kickoff that needs all 1,000 nodes pulling before the job can start, this is the difference between starting on time and starting four minutes late every day. Over a quarter of training runs, that adds up.&lt;/P&gt;
&lt;H3 data-line="84"&gt;2. The surprise: more copies made pulls&amp;nbsp;&lt;EM&gt;slower&lt;/EM&gt;&lt;/H3&gt;
&lt;P data-line="86"&gt;This is the finding that genuinely surprised us. Going from Higher to Very high, the largest configuration we tested, cost us 3 minutes and 26 seconds at P99: 10m 22s climbing back up to 13m 48s. That gave back almost the entire benefit we'd built up over the previous four configurations. Tail storage-read latency at Very high actually came out worse than Baseline.&lt;/P&gt;
&lt;P data-line="88"&gt;The Very high run is where the wheels came off, and the reason is the trade-off underneath. Once storage throttling is gone, more copies stop buying you anything, and the cost of fanning reads across that many backends starts to take over. The throughput distribution shows it clearly. P50 and P75 throughput had been climbing steadily and getting smoother through Mid and Higher, then dropped sharply at Very high while the peak P99/P99.9 spikes came back. Spread the same load across too many backends and it fragments into smaller, less consistent bursts.&lt;/P&gt;
&lt;P data-line="90"&gt;The takeaway is that "more is better" stops being true past the sweet spot, and the failure mode is quiet. You won't see throttling errors. You'll just see your pulls get slower.&lt;/P&gt;
&lt;H3 data-line="92"&gt;3. What we didn't expect: at few copies, the hottest backend is what hurts you&lt;/H3&gt;
&lt;P data-line="94"&gt;At the lowest copy counts, pull traffic wasn't spread evenly across the underlying storage footprint. Some backends absorbed far more traffic than others. As we added copies, that distribution evened out and the hottest backends cooled down.&lt;/P&gt;
&lt;P data-line="96"&gt;The implication is sharp. You can saturate the busiest backend, and trigger throttling, even when the total headroom across all your backends is large in aggregate. What matters is the load on the hottest backend, not the average. That's exactly the failure mode that demand-driven, proactive scaling (described below) is meant to head off before it happens.&lt;/P&gt;
&lt;H2 data-line="100"&gt;So how should you think about this?&lt;/H2&gt;
&lt;P data-line="102"&gt;You don't size copies yourself; ACR manages the storage footprint behind your registry. Still, it helps to understand what moves the sweet spot, because the shape of your own workload is what decides where it lands. The bigger your worst-case concurrent burst (more nodes, larger images, higher concurrency), the more copies per layer it takes to keep pulls off the throttling ceiling, and the further out the sweet spot sits. Smaller workloads may already be sitting on the flat part of the curve.&lt;/P&gt;
&lt;P data-line="104"&gt;One thing is worth saying plainly. The storage footprint underneath is managed by ACR and shared across many registries, so there's no fixed, private storage budget that maps one-to-one to your workload. The sweet spot isn't a number you compute and provision; it's a behavior the platform has to land on for you, which is exactly why we're moving toward demand-driven scaling that handles it automatically.&lt;/P&gt;
&lt;P data-line="106"&gt;That's what brings us to what we're building next.&lt;/P&gt;
&lt;H2 data-line="110"&gt;What's next: proactive, on-demand storage scaling and a caching layer&lt;/H2&gt;
&lt;P data-line="112"&gt;The fixed-copy tests above answer the question "how many should the ACR system provision?" but they assume a single, static answer. Real workloads aren't static. A 1,000-node burst happens at deploy time, not at 3 a.m. on a Tuesday. And no matter how many copies are provisioned, the per-backend storage ceiling still bounds peak deliverable throughput. So we're investing along two complementary directions.&lt;/P&gt;
&lt;H3 data-line="114"&gt;1. Proactive, demand-driven storage scaling&lt;/H3&gt;
&lt;P data-line="116"&gt;We're building a capability that adjusts the number of per-layer copies automatically based on real-time pull demand:&lt;/P&gt;
&lt;UL data-line="118"&gt;
&lt;LI data-line="118"&gt;&lt;STRONG&gt;Proactive, not reactive.&lt;/STRONG&gt;&amp;nbsp;The system scales the storage footprint before concurrent pull pressure pushes any single backend near the throttling threshold, so throttling is prevented before it forms rather than cleaned up after the fact.&lt;/LI&gt;
&lt;LI data-line="119"&gt;&lt;STRONG&gt;On-demand scale-out.&lt;/STRONG&gt;&amp;nbsp;The footprint expands automatically as sustained pull demand grows.&lt;/LI&gt;
&lt;LI data-line="120"&gt;&lt;STRONG&gt;Scale-in when demand subsides.&lt;/STRONG&gt;&amp;nbsp;The footprint contracts so you're not paying for steady-state capacity you only needed during a burst.&lt;/LI&gt;
&lt;LI data-line="121"&gt;&lt;STRONG&gt;Tiering for cold content.&lt;/STRONG&gt;&amp;nbsp;Long-tail, rarely-pulled content can sit on colder storage, so the redundant footprint of frequently-pulled content doesn't pay full hot-storage cost everywhere.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P data-line="123"&gt;The benefit to customers is straightforward: smoother pulls under burst, higher delivered throughput on average, no permanent over-provisioning, and no manual re-tuning as workloads grow.&lt;/P&gt;
&lt;H3 data-line="125"&gt;2. A caching layer to absorb burst beyond the storage ceiling&lt;/H3&gt;
&lt;P data-line="127"&gt;Even a perfectly scaled storage footprint runs into the per-backend egress ceiling at extreme scale. To push past it, we're investing in a caching layer in the registry service that absorbs burst traffic before it ever reaches storage. A pull surge that hits the same set of layers, which is the common case for fleet-wide deployments, can be served largely from cache. That takes a lot of load off any single storage backend and complements the storage scaling above.&lt;/P&gt;
&lt;P data-line="129"&gt;We'll share results from this work in follow-up posts.&lt;/P&gt;
&lt;P data-line="133"&gt;If you have questions about scaling ACR for your workload, or about how we measure storage performance, reach out on the&amp;nbsp;&lt;A href="https://github.com/Azure/acr" data-href="https://github.com/Azure/acr" target="_blank"&gt;Azure Container Registry GitHub repository&lt;/A&gt;.&lt;/P&gt;
&lt;P data-line="135"&gt;&lt;EM&gt;&lt;STRONG&gt;Note:&lt;/STRONG&gt; All results in this post are based on controlled internal testing configurations and are intended to illustrate general scaling behavior rather than prescribe exact configurations.&lt;/EM&gt;&lt;/P&gt;</description>
      <pubDate>Mon, 22 Jun 2026 15:20:26 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/how-many-copies-of-each-layer-does-your-container-registry/ba-p/4529836</guid>
      <dc:creator>payalmahesh</dc:creator>
      <dc:date>2026-06-22T15:20:26Z</dc:date>
    </item>
    <item>
      <title>Controlling Tool Access with APIM MCP Gateway</title>
      <link>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/controlling-tool-access-with-apim-mcp-gateway/ba-p/4529225</link>
      <description>&lt;P&gt;If you've started working with MCP servers in GitHub Copilot, Claude, or any other agent host in an enterprise environment, you've probably hit a similar problem. You want to give your developers access to a useful MCP server, but not every tool it ships with. Maybe one tool is noisy and burns context for no good reason. Maybe one tool calls a paid API. Maybe one tool does something your security team is not comfortable with. The MCP server is all-or-nothing: install it and you get the lot.&lt;/P&gt;
&lt;P&gt;Most MCP servers don't give you a way to switch individual tools off. The MCP spec doesn't define one either. So if you want fine-grained control, you need to put something in front of the server that can see the JSON-RPC traffic and make decisions about it. That something is an MCP gateway, and Azure API Management (APIM) can do the job.&lt;/P&gt;
&lt;H2 data-heading="What an MCP gateway actually is"&gt;What an MCP gateway actually is&lt;/H2&gt;
&lt;P&gt;An MCP gateway sits between the agent (the client) and the MCP server (the backend) and inspects the MCP protocol traffic flowing between them. Because MCP is just JSON-RPC over HTTP or SSE, anything that can do reverse-proxy plus payload inspection can in theory act as a gateway. The interesting bit is what you do with that position in the network.&lt;/P&gt;
&lt;P&gt;If you've worked with API gateways before, the mental model is the same: you're putting a managed entry point in front of one or more backends, and using policy to control what gets through. The differences are that the protocol is JSON-RPC over a long-lived connection, and the things you're filtering aren't routes but tools, prompts, and resources.&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;Out of the box, MCP gives you very little operational control. The protocol assumes a trusted one-to-one relationship between a client and a server. There's no built-in authentication story beyond what&lt;/P&gt;
&lt;P&gt;the transport gives you, no rate limiting, no audit trail beyond the agent's own logs, and no way to share one MCP server safely across multiple users or teams. A gateway is how you fix all of that without modifying the MCP server itself.&lt;/P&gt;
&lt;H2 data-heading="Why you'd use one"&gt;MCP Gateway Functionality&lt;/H2&gt;
&lt;P&gt;The tool access problem is not the only reason to put a gateway in front of an MCP server. The list looks a lot like the list of reasons you'd put any API behind a gateway, with a few MCP-specific twists:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Centralised authentication and authorisation.&lt;/STRONG&gt; Add Entra ID, mTLS, or scoped tokens in front of MCP servers that ship with little more than an API key.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Rate limiting and quota.&lt;/STRONG&gt; Stop a runaway agent loop from hammering a paid upstream API and racking up a bill in minutes.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Logging, audit and observability.&lt;/STRONG&gt; Capture which user invoked which tool with what arguments and ship it to Log Analytics or your SIEM.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Network isolation.&lt;/STRONG&gt; Keep developer machines off the public internet by fronting external MCP servers with private endpoints and fixed egress IPs.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Sharing one MCP server across many clients.&lt;/STRONG&gt; Turn a single-tenant MCP server into a multi-tenant front door with per-user identity and per-team limits.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Policy and governance.&lt;/STRONG&gt; Control which tools are exposed, redact fields, validate arguments, or transform responses before they reach the agent.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Aggregating multiple MCP servers.&lt;/STRONG&gt; Present one logical MCP endpoint that fans out to several backends so agent hosts have a single connection point.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Failure handling and resilience.&lt;/STRONG&gt; Add retries, circuit breakers, and caching of tools/list rather than have every agent host grow its own.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Cost management.&lt;/STRONG&gt; Put caps, alerts, and per-team chargeback on MCP servers that wrap something paid by the call.&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2 data-heading="APIM as the gateway"&gt;APIM as the gateway&lt;/H2&gt;
&lt;P&gt;APIM gives you all of the above, and Microsoft has been adding MCP-specific support over the last few releases. The relevant bits for this post:&lt;/P&gt;
&lt;OL&gt;
&lt;LI&gt;There's a dedicated &lt;STRONG&gt;MCP servers&lt;/STRONG&gt; section in APIM, separate from the standard APIs blade. This is where MCP-aware features live.&lt;/LI&gt;
&lt;LI&gt;You can register an existing MCP server as an &lt;STRONG&gt;external MCP server&lt;/STRONG&gt;. APIM proxies the protocol traffic and applies policies to it.&lt;/LI&gt;
&lt;LI&gt;You can also expose a normal REST API as an MCP server, by adding an MCP layer on top of an existing API.&lt;/LI&gt;
&lt;LI&gt;The standard APIM &lt;STRONG&gt;policy engine&lt;/STRONG&gt; works on MCP traffic.&lt;/LI&gt;
&lt;/OL&gt;
&lt;H2 data-heading="The shape of the problem"&gt;The shape of the problem&lt;/H2&gt;
&lt;P&gt;The MCP protocol has two methods that matter here:&lt;/P&gt;
&lt;OL&gt;
&lt;LI&gt;tools/list is what the client calls when it connects. The server returns the catalogue of tools available, with names, descriptions, and input schemas. The agent uses this to decide what it can do.&lt;/LI&gt;
&lt;LI&gt;tools/call is what the client sends when the agent actually wants to invoke a tool.&lt;/LI&gt;
&lt;/OL&gt;
&lt;P&gt;If you want to hide a tool, you have to deal with both. Filtering tools/list stops the agent ever knowing the tool exists, which is what you usually want. Blocking tools/call stops a determined client (or a tool the agent guessed at) from calling it directly. You need both to be confident the tool is genuinely off-limits.&lt;/P&gt;
&lt;P&gt;For the rest of this post I'll use the &lt;A class="lia-external-url" href="https://learn.microsoft.com/en-us/training/support/mcp" target="_blank"&gt;&lt;STRONG&gt;Microsoft Learn MCP server&lt;/STRONG&gt;&lt;/A&gt; as the example, because it's public, useful, and ships with three tools:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;microsoft_docs_search&lt;/LI&gt;
&lt;LI&gt;microsoft_docs_fetch&lt;/LI&gt;
&lt;LI&gt;microsoft_code_sample_search&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Say I want to allow the first two and block the third. Here's how to do it.&lt;/P&gt;
&lt;H2 data-heading="Setting up APIM as an MCP gateway"&gt;Setting up APIM as an MCP gateway&lt;/H2&gt;
&lt;P&gt;I'm going to skip the bit where you provision APIM. The MCP-specific setup is:&lt;/P&gt;
&lt;OL&gt;
&lt;LI&gt;In your APIM instance, go to &lt;STRONG&gt;MCP Servers&lt;/STRONG&gt; in the left-hand nav (it's its own section, not under APIs).&lt;/LI&gt;
&lt;LI&gt;&lt;img /&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;/LI&gt;
&lt;LI&gt;Add a new MCP server. Choose &lt;STRONG&gt;External MCP server&lt;/STRONG&gt; as the type.&lt;/LI&gt;
&lt;LI&gt;Point it at the Microsoft Learn MCP server endpoint. The transport will be HTTP or SSE depending on the upstream, which APIM handles for you.&lt;/LI&gt;
&lt;LI&gt;Save. APIM now proxies the MCP traffic. Configure the agent to point at the APIM-exposed URL instead of the upstream.&lt;/LI&gt;
&lt;/OL&gt;
&lt;img /&gt;
&lt;P&gt;At this point you have a working passthrough. The next step is the policy.&lt;/P&gt;
&lt;H2 data-heading="Two ways to control tool access"&gt;Two ways to control tool access&lt;/H2&gt;
&lt;P&gt;There are two patterns that work, and the right choice depends on how much maintenance you want to take on.&lt;/P&gt;
&lt;H3 data-heading="Option A: Static allowlist"&gt;Option A: Static allowlist&lt;/H3&gt;
&lt;P&gt;You hardcode the list of tools APIM will expose. APIM intercepts tools/list and returns your fixed list, ignoring whatever the backend says. It also blocks tools/call for anything not on the list.&lt;/P&gt;
&lt;P&gt;Pros: predictable, doesn't read the response body, doesn't care what the upstream changes.&lt;/P&gt;
&lt;P&gt;Cons: you have to maintain the schemas yourself. If Microsoft adds a useful new tool to the Learn MCP server, you won't see it until you update the policy. If they change the input schema for an existing tool, you'll need to update that too.&lt;/P&gt;
&lt;P&gt;This is the option I'd reach for first if I wanted strict control and wasn't expecting the upstream to change much.&lt;/P&gt;
&lt;H3 data-heading="Option B: Dynamic deny-list"&gt;Option B: Dynamic deny-list&lt;/H3&gt;
&lt;P&gt;You let tools/list flow through, then rewrite the response on its way back out, removing the tools you don't want. You also block tools/call for those tools.&lt;/P&gt;
&lt;P&gt;Pros: lower maintenance. New tools appear automatically. Schema changes are picked up.&lt;/P&gt;
&lt;P&gt;Cons: this reads and rewrites context.Response.Body, and &lt;A class="lia-external-url" href="https://learn.microsoft.com/en-us/azure/api-management/expose-existing-mcp-server#configure-policies-for-the-mcp-server" target="_blank"&gt;Microsoft's own guidance for MCP policies&lt;/A&gt; warns that response-body access can interfere with streaming. In practice, tools/list is non-streaming JSON-RPC and this works fine, but you need to test it carefully in your environment, particularly if your agent host is fussy about response handling. This method also requires you to keep on top of when new tools are added and add them to the list if you don't want them used, they will automatically be available to users if you do not.&lt;/P&gt;
&lt;P&gt;Pick this one when you're confident in the upstream and want to minimise toil.&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;H2 data-heading="Option A: the allowlist policy"&gt;Option A: the allowlist policy&lt;/H2&gt;
&lt;P&gt;Here's the full policy for the static allowlist. It goes into the MCP Server policy editor in APIM. You'll notice there's no &amp;lt;base /&amp;gt; in the sections. Some MCP Server policy editors don't include them, and the policy works fine without them.&lt;/P&gt;
&lt;LI-CODE lang="xml"&gt;&amp;lt;policies&amp;gt; &amp;lt;inbound&amp;gt; &amp;lt;choose&amp;gt; &amp;lt;when condition="@{ var body = context.Request.Body.As&amp;lt;Newtonsoft.Json.Linq.JObject&amp;gt;(preserveContent: true); if (body == null) { return false; } var rpcMethod = (string)body["method"]; return string.Equals(rpcMethod, "tools/list", System.StringComparison.OrdinalIgnoreCase); }"&amp;gt; &amp;lt;return-response&amp;gt; &amp;lt;set-status code="200" reason="OK" /&amp;gt; &amp;lt;set-header name="Content-Type" exists-action="override"&amp;gt; &amp;lt;value&amp;gt;application/json&amp;lt;/value&amp;gt; &amp;lt;/set-header&amp;gt; &amp;lt;set-body&amp;gt;@{ var req = context.Request.Body.As&amp;lt;Newtonsoft.Json.Linq.JObject&amp;gt;(preserveContent: true); var id = req != null ? req["id"] : Newtonsoft.Json.Linq.JValue.CreateNull(); var tools = new Newtonsoft.Json.Linq.JArray( new Newtonsoft.Json.Linq.JObject( new Newtonsoft.Json.Linq.JProperty("name", "microsoft_docs_search"), new Newtonsoft.Json.Linq.JProperty("description", "Search official Microsoft/Azure documentation"), new Newtonsoft.Json.Linq.JProperty("inputSchema", new Newtonsoft.Json.Linq.JObject( new Newtonsoft.Json.Linq.JProperty("type", "object"), new Newtonsoft.Json.Linq.JProperty("properties", new Newtonsoft.Json.Linq.JObject( new Newtonsoft.Json.Linq.JProperty("query", new Newtonsoft.Json.Linq.JObject( new Newtonsoft.Json.Linq.JProperty("type", "string") )) )) )) ), new Newtonsoft.Json.Linq.JObject( new Newtonsoft.Json.Linq.JProperty("name", "microsoft_docs_fetch"), new Newtonsoft.Json.Linq.JProperty("description", "Fetch a Microsoft documentation page as markdown"), new Newtonsoft.Json.Linq.JProperty("inputSchema", new Newtonsoft.Json.Linq.JObject( new Newtonsoft.Json.Linq.JProperty("type", "object"), new Newtonsoft.Json.Linq.JProperty("properties", new Newtonsoft.Json.Linq.JObject( new Newtonsoft.Json.Linq.JProperty("url", new Newtonsoft.Json.Linq.JObject( new Newtonsoft.Json.Linq.JProperty("type", "string") )) )), new Newtonsoft.Json.Linq.JProperty("required", new Newtonsoft.Json.Linq.JArray("url")) )) ) ); var response = new Newtonsoft.Json.Linq.JObject( new Newtonsoft.Json.Linq.JProperty("jsonrpc", "2.0"), new Newtonsoft.Json.Linq.JProperty("id", id), new Newtonsoft.Json.Linq.JProperty("result", new Newtonsoft.Json.Linq.JObject( new Newtonsoft.Json.Linq.JProperty("tools", tools) )) ); return response.ToString(Newtonsoft.Json.Formatting.None); }&amp;lt;/set-body&amp;gt; &amp;lt;/return-response&amp;gt; &amp;lt;/when&amp;gt; &amp;lt;when condition="@{ var body = context.Request.Body.As&amp;lt;Newtonsoft.Json.Linq.JObject&amp;gt;(preserveContent: true); if (body == null) { return false; } var rpcMethod = (string)body["method"]; var toolName = (string)body["params"]?["name"]; return string.Equals(rpcMethod, "tools/call", System.StringComparison.OrdinalIgnoreCase) &amp;amp;&amp;amp; string.Equals(toolName, "microsoft_code_sample_search", System.StringComparison.OrdinalIgnoreCase); }"&amp;gt; &amp;lt;return-response&amp;gt; &amp;lt;set-status code="200" reason="OK" /&amp;gt; &amp;lt;set-header name="Content-Type" exists-action="override"&amp;gt; &amp;lt;value&amp;gt;application/json&amp;lt;/value&amp;gt; &amp;lt;/set-header&amp;gt; &amp;lt;set-body&amp;gt;@{ var req = context.Request.Body.As&amp;lt;Newtonsoft.Json.Linq.JObject&amp;gt;(preserveContent: true); var id = req != null ? req["id"] : Newtonsoft.Json.Linq.JValue.CreateNull(); var error = new Newtonsoft.Json.Linq.JObject( new Newtonsoft.Json.Linq.JProperty("jsonrpc", "2.0"), new Newtonsoft.Json.Linq.JProperty("id", id), new Newtonsoft.Json.Linq.JProperty("error", new Newtonsoft.Json.Linq.JObject( new Newtonsoft.Json.Linq.JProperty("code", -32601), new Newtonsoft.Json.Linq.JProperty("message", "Tool disabled by API Management policy: microsoft_code_sample_search") )) ); return error.ToString(Newtonsoft.Json.Formatting.None); }&amp;lt;/set-body&amp;gt; &amp;lt;/return-response&amp;gt; &amp;lt;/when&amp;gt; &amp;lt;/choose&amp;gt; &amp;lt;/inbound&amp;gt; &amp;lt;backend /&amp;gt; &amp;lt;outbound /&amp;gt; &amp;lt;on-error /&amp;gt; &amp;lt;/policies&amp;gt;&lt;/LI-CODE&gt;
&lt;P&gt;It's a chunk of XML, but it's doing only three things. Here's what each part is for.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;The outer &amp;lt;choose&amp;gt; and the two &amp;lt;when&amp;gt; branches.&lt;/STRONG&gt; This is just an if/else if. APIM peeks at the inbound JSON-RPC request and decides which branch to fire. The first branch handles tools/list. The second handles tools/call for the specific tool I want to block. If neither matches, nothing happens in the policy and the request flows through to the backend as normal. So tools/call for the allowed tools, plus initialize, ping, and anything else MCP throws at the gateway, all pass through untouched.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;The tools/list branch: synthesise the catalogue.&lt;/STRONG&gt; When the agent asks for the tool list, APIM never forwards the call. Instead, the policy reads the id off the incoming request (so the response correlates correctly), builds a JArray containing only the tools I want to expose, with their names, descriptions, and inputSchema blocks, wraps that in a JSON-RPC result envelope, and returns it directly with &amp;lt;return-response&amp;gt;. The backend never sees the request. That's the bit that makes Option A bulletproof: there is no upstream behaviour that can leak through, because the upstream isn't involved.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;The tools/call branch: hard-block the disallowed tool.&lt;/STRONG&gt; When the agent tries to call microsoft_code_sample_search directly (whether it guessed at it, cached it from a previous run, or someone added it to a config file), the policy short-circuits with a JSON-RPC error. The error code is -32601, which the spec defines as "Method not found", and the message says plainly that the tool was disabled by APIM. Well-behaved agents will surface that to the user and move on. Without this branch, hiding the tool from tools/list would only stop honest clients.&lt;/P&gt;
&lt;P&gt;A couple of incidental things worth knowing. The preserveContent: true argument on context.Request.Body.As&amp;lt;...&amp;gt;() is important: without it, reading the body consumes it and the rest of the pipeline gets nothing. And the OrdinalIgnoreCase comparisons are belt-and-braces: MCP method names are case-sensitive in the spec, but agents in the wild are inconsistent.&lt;/P&gt;
&lt;H2 data-heading="Option B: the deny-list policy"&gt;Option B: the deny-list policy&lt;/H2&gt;
&lt;P&gt;Same scenario, different approach. Let tools/list flow through to the backend and rewrite the response on the way out. Still block tools/call for the disallowed tool.&lt;/P&gt;
&lt;LI-CODE lang="xml"&gt;```xml &amp;lt;policies&amp;gt; &amp;lt;inbound&amp;gt; &amp;lt;choose&amp;gt; &amp;lt;when condition="@{ var body = context.Request.Body.As&amp;lt;Newtonsoft.Json.Linq.JObject&amp;gt;(preserveContent: true); if (body == null) { return false; } var rpcMethod = (string)body["method"]; var toolName = (string)body["params"]?["name"]; return string.Equals(rpcMethod, "tools/call", System.StringComparison.OrdinalIgnoreCase) &amp;amp;&amp;amp; string.Equals(toolName, "microsoft_code_sample_search", System.StringComparison.OrdinalIgnoreCase); }"&amp;gt; &amp;lt;return-response&amp;gt; &amp;lt;set-status code="200" reason="OK" /&amp;gt; &amp;lt;set-header name="Content-Type" exists-action="override"&amp;gt; &amp;lt;value&amp;gt;application/json&amp;lt;/value&amp;gt; &amp;lt;/set-header&amp;gt; &amp;lt;set-body&amp;gt;@{ var req = context.Request.Body.As&amp;lt;Newtonsoft.Json.Linq.JObject&amp;gt;(preserveContent: true); var id = req != null ? req["id"] : Newtonsoft.Json.Linq.JValue.CreateNull(); var error = new Newtonsoft.Json.Linq.JObject( new Newtonsoft.Json.Linq.JProperty("jsonrpc", "2.0"), new Newtonsoft.Json.Linq.JProperty("id", id), new Newtonsoft.Json.Linq.JProperty("error", new Newtonsoft.Json.Linq.JObject( new Newtonsoft.Json.Linq.JProperty("code", -32601), new Newtonsoft.Json.Linq.JProperty("message", "Tool disabled by API Management policy: microsoft_code_sample_search") )) ); return error.ToString(Newtonsoft.Json.Formatting.None); }&amp;lt;/set-body&amp;gt; &amp;lt;/return-response&amp;gt; &amp;lt;/when&amp;gt; &amp;lt;/choose&amp;gt; &amp;lt;/inbound&amp;gt; &amp;lt;backend /&amp;gt; &amp;lt;outbound&amp;gt; &amp;lt;choose&amp;gt; &amp;lt;when condition="@{ var req = context.Request.Body.As&amp;lt;Newtonsoft.Json.Linq.JObject&amp;gt;(preserveContent: true); if (req == null) { return false; } var rpcMethod = (string)req["method"]; return string.Equals(rpcMethod, "tools/list", System.StringComparison.OrdinalIgnoreCase); }"&amp;gt; &amp;lt;set-body&amp;gt;@{ var bodyText = context.Response.Body.As&amp;lt;string&amp;gt;(preserveContent: true); if (string.IsNullOrEmpty(bodyText)) { return bodyText; } var resp = Newtonsoft.Json.Linq.JObject.Parse(bodyText); var tools = resp["result"]?["tools"] as Newtonsoft.Json.Linq.JArray; if (tools == null) { return bodyText; } var filtered = new Newtonsoft.Json.Linq.JArray(); foreach (var t in tools) { var name = (string)t?["name"]; if (!string.Equals(name, "microsoft_code_sample_search", System.StringComparison.OrdinalIgnoreCase)) { filtered.Add(t); } } ((Newtonsoft.Json.Linq.JObject)resp["result"])["tools"] = filtered; return resp.ToString(Newtonsoft.Json.Formatting.None); }&amp;lt;/set-body&amp;gt; &amp;lt;/when&amp;gt; &amp;lt;/choose&amp;gt; &amp;lt;/outbound&amp;gt; &amp;lt;on-error /&amp;gt; &amp;lt;/policies&amp;gt;&lt;/LI-CODE&gt;
&lt;P&gt;This policy splits the work across inbound and outbound. The shape is different from Option A, but again it's only doing a few things.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Inbound: block direct calls to the disallowed tool.&lt;/STRONG&gt; This block is identical to the one in Option A. If the agent tries to invoke microsoft_code_sample_search directly, APIM returns the JSON-RPC -32601 error and the request never reaches the backend. Everything else (including tools/list and calls to the allowed tools) carries on through to the upstream MCP server.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Backend: nothing custom.&lt;/STRONG&gt; The empty &amp;lt;backend /&amp;gt; is deliberate. We want the upstream server to handle every request that wasn't blocked in inbound, including tools/list. APIM forwards it, the server returns its full catalogue, and we get a chance to rewrite the response on the way back.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Outbound: filter the catalogue on the way out.&lt;/STRONG&gt; This is the bit that does the dynamic work. The &amp;lt;when&amp;gt; condition checks the &lt;EM&gt;original request&lt;/EM&gt; (not the response) to see if this was a tools/list call, because we only want to rewrite responses to that specific method. If it was, the policy reads the response body as a string, parses it as JSON, walks the result.tools array, builds a new array containing everything &lt;EM&gt;except&lt;/EM&gt; the deny-listed tool, swaps it back into the response object, and writes the modified JSON back with &amp;lt;set-body&amp;gt;. For any other method, the response flows through untouched.&lt;/P&gt;
&lt;P&gt;The reason this option is more fragile than Option A is right there in that last paragraph: it reads and rewrites context.Response.Body. &lt;A class="lia-external-url" href="https://learn.microsoft.com/en-us/azure/api-management/expose-existing-mcp-server#configure-policies-for-the-mcp-server" target="_blank"&gt;Microsoft's own guidance for MCP policies&lt;/A&gt; flags that response-body access can interfere with streaming. For non-streaming JSON-RPC like tools/list, this works fine in practice, but if you ever extend this pattern to filter streamed responses (resource updates, long-running tool calls), you'll need to think much harder about it. For the narrow case of trimming tools/list, it's a reasonable trade for the lower maintenance cost.&lt;/P&gt;
&lt;H2 data-heading="Validating it works"&gt;Validating it works&lt;/H2&gt;
&lt;P&gt;Once the policy is saved, hit the APIM-exposed MCP endpoint with the agent of your choice and check three things:&lt;/P&gt;
&lt;OL&gt;
&lt;LI&gt;tools/list returns only microsoft_docs_search and microsoft_docs_fetch. The third tool should not appear.&lt;/LI&gt;
&lt;LI&gt;tools/call for microsoft_code_sample_search returns a JSON-RPC error with code -32601.&lt;/LI&gt;
&lt;LI&gt;The two allowed tools still actually work end-to-end.&lt;/LI&gt;
&lt;/OL&gt;
&lt;H2 data-heading="Which one should you use"&gt;Which one should you use&lt;/H2&gt;
&lt;P&gt;Default to Option A (the static allowlist) unless you have a specific reason not to. It's the most predictable and it doesn't touch the response body, which keeps you well clear of the streaming caveat. You will have to update it when the upstream tool catalogue changes, but this does give you greater control of when you make those changes available and creates a conscious decision to allow them, or not.&lt;/P&gt;
&lt;P&gt;Reach for Option B when the upstream changes frequently, or when you'd rather block specific known-bad tools and let everything else through. Test it carefully and watch the agent's behaviour for any sign of streaming or framing issues.&lt;/P&gt;
&lt;P&gt;If you've got a fully internal MCP server you control, the cleanest answer is to fix the tool list at the source and skip the gateway changes altogether. But for third-party MCP servers, this is an approach you can apply without changing the underlying MCP server.&lt;/P&gt;</description>
      <pubDate>Thu, 18 Jun 2026 14:44:22 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/controlling-tool-access-with-apim-mcp-gateway/ba-p/4529225</guid>
      <dc:creator>samcogan</dc:creator>
      <dc:date>2026-06-18T14:44:22Z</dc:date>
    </item>
    <item>
      <title>ArchAngel: Skilling the next developer generation for the Agentic transformation.</title>
      <link>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/archangel-skilling-the-next-developer-generation-for-the-agentic/ba-p/4525836</link>
      <description>&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;AI is transforming the SDLC at speed but there's a quieter question following close behind. If the code is being written for your junior developers, when do they learn the skills to become senior ones and how can they use these tools without losing understanding of what they've made? Most tools try to solve this by catching issues after the commit, but by then the teaching moment is already gone. What if your developers could learn why something is wrong and how to fix it, as they write it?&lt;/P&gt;
&lt;P&gt;ArchAngel is built around a simple idea: What if your team’s best engineering practices could exist directly inside the IDE and guide developers as they work? It turns your team's collective wisdom into a mentor that scales to every new hire, every sprint, every repo.&lt;/P&gt;
&lt;P&gt;GitHub Copilot has led to great strides in reducing developer toil. It allows teams to move faster, automate repetitive work, and spend more time on higher-level design decisions that drive real impact. As these capabilities evolve, especially with more agentic workflows, developers now have even more powerful ways to generate and iterate on code.&lt;/P&gt;
&lt;P&gt;The next challenge for engineering teams isn’t adoption. It’s how to make the most of these capabilities while maintaining strong engineering standards, consistency, and shared understanding across teams. Even with powerful tools in place, teams still face familiar challenges. Standards are often spread across repositories, documentation, and conversations. Best practices evolve over time but aren’t always easy to discover or apply consistently. Reviews can become repetitive, and senior engineers spend a lot of time reinforcing patterns rather than focusing on system design.&lt;/P&gt;
&lt;P&gt;AI accelerates development, but it doesn’t automatically understand how your team builds software, the architectural decisions, trade-offs, and conventions that make systems consistent over time. Technically correct code doesn't mean its organisationally aligned.&lt;/P&gt;
&lt;H4&gt;&lt;SPAN class="lia-text-color-10"&gt;&lt;U&gt;&lt;STRONG&gt;__________________________________________________________________&lt;/STRONG&gt;&lt;/U&gt;&lt;/SPAN&gt;&lt;/H4&gt;
&lt;H4&gt;&lt;STRONG&gt;The Solution:&lt;/STRONG&gt;&lt;/H4&gt;
&lt;P&gt;ArchAngel is built for and by developers. It provides a developer driven, AI coding assistant that teaches without taking autonomy. Rather than waiting for devs to commit their code to pick up issues, ArchAngel sits beside through the coding process to guide and provide iterative live feedback. By connecting ArchAngel to your project bases, ArchAngel can be grounded in your 'golden repositories'; The agreed best practices, existing approved repos and organisational standards. ArchAngel can also generate Code Style and Wiki Docs for a quick look summary guide to your best practices, prompting further research.&lt;/P&gt;
&lt;P&gt;Let's define the tech stack behind this and what makes ArchAngel work:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Semantic Kernel and Microsoft Agent Framework (MAF)&amp;nbsp;&lt;/STRONG&gt;- Semantic Kernel is a lightweight, open-source development kit that lets you easily build AI agents and integrate the latest AI models. MAF is an orchestration framework that is used to create agents and conversation history to create and support iterative design. Served via Foundry.
&lt;UL&gt;
&lt;LI&gt;&lt;A href="https://learn.microsoft.com/en-us/semantic-kernel/overview/" target="_blank" rel="noopener"&gt;Introduction to Semantic Kernel | Microsoft Learn&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A href="https://learn.microsoft.com/en-us/agent-framework/overview/?pivots=programming-language-csharp" target="_blank" rel="noopener"&gt;Microsoft Agent Framework Overview | Microsoft Learn&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A href="https://learn.microsoft.com/en-us/azure/foundry/" target="_blank" rel="noopener"&gt;Microsoft Foundry documentation | Microsoft Learn&lt;/A&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG style="color: rgb(30, 30, 30);"&gt;Language Server Protocol (LSP)&lt;/STRONG&gt;&lt;SPAN style="color: rgb(30, 30, 30);"&gt; - LSP allows you to invoke ArchAngel from any IDE. For VSCode, command palette is used to invoke certain functionality for ArchAngel such as document generation.&amp;nbsp;&lt;/SPAN&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;SPAN style="color: rgb(30, 30, 30);"&gt;&lt;A href="https://microsoft.github.io/language-server-protocol/" target="_blank" rel="noopener"&gt;Official page for Language Server Protocol&lt;/A&gt;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG style="color: rgb(30, 30, 30);"&gt;SQLite&amp;nbsp;&lt;/STRONG&gt;&lt;SPAN style="color: rgb(30, 30, 30);"&gt;- Store your search database locally with SQLite.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG style="color: rgb(30, 30, 30);"&gt;Config&amp;nbsp;&lt;/STRONG&gt;&lt;SPAN style="color: rgb(30, 30, 30);"&gt;- Setup just like your other dev tools. For VScode, simply load your base repos into archangel.json, authenticate with github and run the "Index from config file" in your command palette.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;H4&gt;&lt;SPAN class="lia-text-color-10"&gt;&lt;U&gt;&lt;STRONG&gt;__________________________________________________________________&lt;/STRONG&gt;&lt;/U&gt;&lt;/SPAN&gt;&lt;/H4&gt;
&lt;H4&gt;&lt;STRONG&gt;Meet Priya: A Senior Software Engineer&lt;/STRONG&gt;&lt;/H4&gt;
&lt;img /&gt;
&lt;H5&gt;&lt;STRONG&gt;Responsibilities&lt;/STRONG&gt;:&lt;/H5&gt;
&lt;UL&gt;
&lt;LI&gt;
&lt;P&gt;Design &amp;amp; build scalable software — Architect and deliver high-quality, maintainable systems from concept through to production.&lt;/P&gt;
&lt;/LI&gt;
&lt;LI&gt;
&lt;P&gt;Drive engineering excellence — Lead code reviews, enforce best practices, and champion CI/CD, testing, and observability standards.&lt;/P&gt;
&lt;/LI&gt;
&lt;LI&gt;
&lt;P&gt;Mentor junior engineers — Coach and develop less experienced team members, fostering technical growth and a collaborative culture.&lt;/P&gt;
&lt;/LI&gt;
&lt;LI&gt;
&lt;P&gt;Collaborate cross-functionally — Work with Product, Design, and stakeholders to translate business needs into robust technical solutions.&lt;/P&gt;
&lt;/LI&gt;
&lt;LI&gt;
&lt;P&gt;Innovate &amp;amp; improve continuously — Stay ahead of industry trends, reduce technical debt, and drive adoption of tools and processes that elevate team productivity.&lt;/P&gt;
&lt;/LI&gt;
&lt;/UL&gt;
&lt;H4&gt;&lt;STRONG&gt;Meet Joe: A Junior Software Engineer&lt;/STRONG&gt;&lt;/H4&gt;
&lt;img /&gt;
&lt;H5&gt;&lt;STRONG&gt;Responsibilities&lt;/STRONG&gt;:&lt;/H5&gt;
&lt;UL&gt;
&lt;LI&gt;Write and maintain clean code — Develop, test, and debug features under guidance from senior engineers, following established coding standards.&lt;/LI&gt;
&lt;LI&gt;Learn the codebase &amp;amp; tech stack — Ramp up quickly on existing systems, tools, and workflows to become a productive contributor.&lt;/LI&gt;
&lt;LI&gt;Participate in code reviews — Submit code for review and actively learn from feedback, while reviewing peers' work to build technical judgement.&lt;/LI&gt;
&lt;LI&gt;Collaborate with the team — Contribute to sprint ceremonies, ask questions, flag blockers early, and communicate progress clearly.&lt;/LI&gt;
&lt;LI&gt;Invest in personal growth — Pursue learning through documentation, certifications, pair programming, and internal knowledge-sharing sessions.&lt;/LI&gt;
&lt;/UL&gt;
&lt;H4&gt;&lt;STRONG&gt;A day in the life&lt;/STRONG&gt;&lt;/H4&gt;
&lt;P&gt;Priya is responsible for Joe's development and guiding him to progress his career, however, Priya is extremely busy and she is managing two other junior devs. So reviewing each junior's code to identify the architectural antipatterns and documentation decisions that deviate from best practice, takes time away from the senior work she brings most value to.&amp;nbsp;&lt;/P&gt;
&lt;H4&gt;&lt;STRONG&gt;Enter ArchAngel&lt;/STRONG&gt;&lt;/H4&gt;
&lt;P&gt;New hire/Junior joins 4 months into the project with no clue what repos are being used as an example? The dev team used ArchAngel to create Code Style and Wiki Docs so the junior can at a glance view a few of the standards from the repos. The dev team also has the 'golden repos' linked in the ArchAngel config file, so now all the junior has to do is clone, index and start coding. The junior devs use ArchAngel in their VSCode IDE and receive feedback and critique that is educational directly in their IDE as they code, before anything is committed for formal review. Repository informed chat, code completions and docs make onboarding, learning and guiding junior devs an asynchronous task. Giving Priya some breathing room and improving the quality of the PRs her juniors produce!&lt;/P&gt;
&lt;P&gt;The ArchAngel's educational, constructive criticism and repository-backed way of reviewing code can also be an asset to learn the PR process itself! Learning what ArchAngel looks out for each time, helps to teach juniors to look out for it themselves in PRs and how to critique it with possible alternatives.&lt;/P&gt;
&lt;H4&gt;&lt;SPAN class="lia-text-color-10"&gt;&lt;U&gt;&lt;STRONG&gt;__________________________________________________________________&lt;/STRONG&gt;&lt;/U&gt;&lt;/SPAN&gt;&lt;/H4&gt;
&lt;H3&gt;&lt;STRONG&gt;Example Architecture:&lt;/STRONG&gt;&lt;/H3&gt;
&lt;img /&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;OL&gt;
&lt;LI&gt;&lt;STRONG&gt;Project Repo&lt;/STRONG&gt;: Your project repository, where your team syncs your config files. Setup for success at a team level.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Cloud Environment&lt;/STRONG&gt;: Where ArchAngel's brainpower comes from. Secure, scale, monitor and govern with the Azure platform to align organisationally.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;User Environment&lt;/STRONG&gt;: Where you and your teams code, build and create the software that powers the business. Fit to your dev team and extend to your tools.&amp;nbsp;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Golden Repos&lt;/STRONG&gt;: Your golden base of knowledge. This is the indexed and cited source of your coding standards, the team's guiding principle knowledge and the codebases that your team is striving to match at a code quality and standards level.&lt;/LI&gt;
&lt;/OL&gt;
&lt;H4&gt;&lt;STRONG&gt;Next Steps: Making ArchAngel For You&lt;/STRONG&gt;&lt;/H4&gt;
&lt;P&gt;&lt;STRONG&gt;Configure + Customise ArchAngel to suit your development environment!&amp;nbsp;&lt;/STRONG&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;SPAN style="color: rgb(30, 30, 30);"&gt;&lt;STRONG&gt;VNETs (Virtual Networks)&lt;/STRONG&gt; enable secure networking configurations within a cloud environment. Enabling your resources and any tools you wish to setup to communicate in secure channels.&amp;nbsp;&lt;/SPAN&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;SPAN style="color: rgb(30, 30, 30);"&gt;&lt;A href="https://learn.microsoft.com/en-us/azure/virtual-network/" target="_blank" rel="noopener"&gt;Azure Virtual Network Documentation - Tutorials, quickstarts, API references | Microsoft Learn&lt;/A&gt;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN style="color: rgb(30, 30, 30);"&gt;&lt;STRONG&gt;Microsoft Foundry&lt;/STRONG&gt; - Engage Foundry's full toolset to make ArchAngel customisable and compliant for your organisation.&lt;/SPAN&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;SPAN style="color: rgb(30, 30, 30);"&gt;&lt;A href="https://learn.microsoft.com/en-us/azure/foundry/" target="_blank" rel="noopener"&gt;Microsoft Foundry documentation | Microsoft Learn&lt;/A&gt;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN style="color: rgb(30, 30, 30);"&gt;&lt;STRONG&gt;Foundry Tools&lt;/STRONG&gt;: &lt;A href="https://learn.microsoft.com/en-us/azure/ai-services/what-are-ai-services" target="_blank" rel="noopener"&gt;What are Foundry Tools? - Foundry Tools | Microsoft Learn&lt;/A&gt;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;API Management (AI Gateway)&lt;/STRONG&gt; - Use AI Gateway and APIM to secure, scale, monitor and govern your agents and connections to Microsoft Foundry.
&lt;UL&gt;
&lt;LI&gt;&lt;A href="https://learn.microsoft.com/en-us/azure/api-management/" target="_blank" rel="noopener"&gt;API Management documentation | Microsoft Learn&lt;/A&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;LSP (Language Server Protocol)&amp;nbsp;&lt;/STRONG&gt;- Add custom event and event handlers through the LSP channels across IDEs.&amp;nbsp;
&lt;UL&gt;
&lt;LI&gt;&lt;A href="https://microsoft.github.io/language-server-protocol/" target="_blank" rel="noopener"&gt;Official page for Language Server Protocol&lt;/A&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Customisable Document Generation&lt;/STRONG&gt; - Refine the prompts that are used to create the documents and format as you wish! The base version is a skimmed document with a few snippets as source designed to be simple to extend.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Find the github repo here:&amp;nbsp;&lt;A href="https://github.com/rohitmadhavk/ArchAngel" target="_blank" rel="noopener"&gt;rohitmadhavk/ArchAngel: An education coding assistant to help junior devs learn best practice&lt;/A&gt;&lt;/P&gt;</description>
      <pubDate>Mon, 15 Jun 2026 15:24:42 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/archangel-skilling-the-next-developer-generation-for-the-agentic/ba-p/4525836</guid>
      <dc:creator>RohitMadhavKrishnan</dc:creator>
      <dc:date>2026-06-15T15:24:42Z</dc:date>
    </item>
    <item>
      <title>Debug App Startup Faster on Azure App Service for Linux with Startup Logs</title>
      <link>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/debug-app-startup-faster-on-azure-app-service-for-linux-with/ba-p/4527521</link>
      <description>&lt;P&gt;When an app fails to start on Azure App Service for Linux, one of the first things you need is visibility into what happened during startup. This can include container initialization, runtime setup, startup command execution, application output, and warmup probe results.&lt;/P&gt;
&lt;P&gt;To make this easier, we have added new Azure CLI commands that let you list and view App Service startup logs directly from the command line.&lt;/P&gt;
&lt;H2&gt;List available startup logs&lt;/H2&gt;
&lt;P&gt;You can list startup logs for an app using:&lt;/P&gt;
&lt;LI-CODE lang="bash"&gt;az webapp log startup list \
  --name &amp;lt;app-name&amp;gt; \
  --resource-group &amp;lt;resource-group&amp;gt;&lt;/LI-CODE&gt;
&lt;P&gt;The output shows whether the startup attempt succeeded or failed, along with the instance name and log file size. This helps you quickly identify the right log file, especially when there are multiple startup attempts across different instances.&lt;BR /&gt;&lt;BR /&gt;&lt;/P&gt;
&lt;img /&gt;
&lt;H2&gt;Show startup log content&lt;/H2&gt;
&lt;P&gt;To view the latest startup log, run:&lt;/P&gt;
&lt;LI-CODE lang="bash"&gt;az webapp log startup show \
  --name &amp;lt;app-name&amp;gt; \
  --resource-group &amp;lt;resource-group&amp;gt;&lt;/LI-CODE&gt;
&lt;P&gt;You can also view a specific log file by name:&lt;/P&gt;
&lt;LI-CODE lang="bash"&gt;az webapp log startup show \
  --name &amp;lt;app-name&amp;gt; \
  --resource-group &amp;lt;resource-group&amp;gt; \
  --log-file-name &amp;lt;log-file-name&amp;gt;&lt;/LI-CODE&gt;
&lt;P&gt;The log content includes startup events from the platform and the application. For example, you can see the container image being pulled, the startup script being generated, the app command being run, and the warmup probe result.&lt;/P&gt;
&lt;P&gt;In a successful startup, the log shows that the site startup probe succeeded and the site started successfully.&lt;/P&gt;
&lt;img /&gt;
&lt;H2&gt;Failure logs are prioritized by default&lt;/H2&gt;
&lt;P&gt;When you run&amp;nbsp;az webapp log startup show&amp;nbsp;without specifying a log file name, the command automatically prefers failure logs from the most recent date.&lt;/P&gt;
&lt;P&gt;This helps reduce the time spent looking for the right log when debugging startup failures. Instead of manually searching through multiple files, you can run one command and immediately see the most relevant failure details.&lt;/P&gt;
&lt;P&gt;For example, if the app fails because the worker process does not start within the allotted time, the log shows the timeout details and the platform actions taken during startup cancellation.&lt;/P&gt;
&lt;img /&gt;
&lt;H2&gt;Better hints for common startup failures&lt;/H2&gt;
&lt;P&gt;The command also includes improved handling for common failure scenarios, including runtime startup failures and container startup timeouts.&lt;/P&gt;
&lt;P&gt;For example, if the app starts but does not respond on the expected port, the startup log may show application output such as:&lt;/P&gt;
&lt;LI-CODE lang="bash"&gt;listening on 3000 (wrong port)&lt;/LI-CODE&gt;
&lt;P&gt;while the platform is expecting the app to respond on a different port. This makes it much easier to understand why the warmup probe failed.&lt;/P&gt;
&lt;H2&gt;Slot support&lt;/H2&gt;
&lt;P&gt;The startup log commands also support deployment slots.&lt;/P&gt;
&lt;P&gt;To list startup logs for a slot:&lt;/P&gt;
&lt;LI-CODE lang=""&gt;az webapp log startup list \
  --name &amp;lt;app-name&amp;gt; \
  --resource-group &amp;lt;resource-group&amp;gt; \
  --slot &amp;lt;slot-name&amp;gt;&lt;/LI-CODE&gt;
&lt;P&gt;To show startup logs for a slot:&lt;/P&gt;
&lt;LI-CODE lang=""&gt;az webapp log startup show \
  --name &amp;lt;app-name&amp;gt; \
  --resource-group &amp;lt;resource-group&amp;gt; \
  --slot &amp;lt;slot-name&amp;gt;&lt;/LI-CODE&gt;
&lt;P&gt;This is useful when debugging slot-specific startup issues before swapping traffic to production.&lt;/P&gt;
&lt;H2&gt;Summary&lt;/H2&gt;
&lt;P&gt;The new&amp;nbsp;az webapp log startup&amp;nbsp;commands make it easier to inspect startup behavior for Azure App Service for Linux apps directly from Azure CLI.&lt;/P&gt;
&lt;P&gt;These commands are currently in preview. Try them out the next time you need to understand why your App Service Linux app did or did not start successfully.&lt;/P&gt;</description>
      <pubDate>Thu, 11 Jun 2026 12:34:17 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/debug-app-startup-faster-on-azure-app-service-for-linux-with/ba-p/4527521</guid>
      <dc:creator>TulikaC</dc:creator>
      <dc:date>2026-06-11T12:34:17Z</dc:date>
    </item>
    <item>
      <title>How ARM Tracks Work That Takes Hours</title>
      <link>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/how-arm-tracks-work-that-takes-hours/ba-p/4526930</link>
      <description>&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;By &lt;A class="lia-external-url" href="https://www.linkedin.com/in/aravgoyal" target="_blank"&gt;Arav Goyal&lt;/A&gt;,&amp;nbsp;&lt;A class="lia-external-url" href="https://www.linkedin.com/in/joy-shah/" target="_blank"&gt;Joy Shah&lt;/A&gt;, &lt;A class="lia-external-url" href="https://www.linkedin.com/in/michaelchengcsy/" target="_blank"&gt;Michael Cheng&lt;/A&gt;, &lt;A class="lia-external-url" href="https://www.linkedin.com/in/maniksikka/" target="_blank"&gt;Manik Sikka&lt;/A&gt;, &lt;A class="lia-external-url" href="https://www.linkedin.com/in/thetechpm/" target="_blank"&gt;Jenny Hunter&lt;/A&gt;, &lt;A class="lia-external-url" href="https://www.linkedin.com/in/johnsonshi/" target="_blank"&gt;Johnson Shi&lt;/A&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN data-contrast="auto"&gt;Introduction&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;When a user creates, updates, or deletes a resource in Azure, the request flows through Azure Resource Manager (ARM) before reaching the service that actually owns the resource. For operations that complete in milliseconds, the request and response fit cleanly into a single synchronous HTTP exchange. For operations that take seconds, minutes, or hours, this is not possible: HTTP connections cannot be held open that long, and the user's client needs a way to track the work asynchronously. ARM and Azure's resource providers (RPs) implement this through a standard long-running operation (LRO) protocol built on the &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Azure-AsyncOperation&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; and &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Location&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; HTTP headers, status URLs, and provisioning states. This post describes that protocol end-to-end and traces a request from the Portal or CLI all the way through to terminal completion.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2 aria-level="2"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;Key Takeaways&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="24" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559683&amp;quot;:0,&amp;quot;335559684&amp;quot;:-2,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="1" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;All Azure control plane traffic (Portal, CLI, PowerShell, SDKs, REST API) is routed through ARM, which forwards requests to the appropriate resource provider.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:0}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="24" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559683&amp;quot;:0,&amp;quot;335559684&amp;quot;:-2,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="2" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Operations that cannot complete inside a single HTTP request are returned as long-running operations, marked by an HTTP 201 or 202 response and a status URL the caller polls until completion.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:0}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="24" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559683&amp;quot;:0,&amp;quot;335559684&amp;quot;:-2,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="3" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;The two primary LRO patterns use the &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Azure-AsyncOperation&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; header (returns operation status) and the &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Location&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; header (returns the resource itself once complete). Both are guided by a &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Retry-After&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; value when the resource provider supplies one.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:0}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="24" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559683&amp;quot;:0,&amp;quot;335559684&amp;quot;:-2,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="4" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Clients should prefer the &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Azure-AsyncOperation&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; URL when it is present, because the structured status response is more informative than the implicit "still 202" signal from polling Location alone.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:0}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="24" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559683&amp;quot;:0,&amp;quot;335559684&amp;quot;:-2,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="5" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Many Azure resources also expose a &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;provisioningState&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; property that reaches a terminal value when the operation completes, providing a secondary signal in addition to the async operation status URL.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:0}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2 aria-level="2"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;Background: ARM as the Control Plane&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Azure Resource Manager is the deployment and management service for Azure. When a user issues a control plane request through any Azure interface (the Portal, the Azure CLI, PowerShell, an SDK, or a direct REST API call), the request reaches ARM at &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;management.azure.com&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;. ARM authenticates the request, authorizes it against the appropriate role assignments and policies, and then forwards it to the resource provider that owns the resource type in question. Resource providers are the Azure services that actually implement specific resource types. &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Microsoft.Compute&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; provides virtual machines, &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Microsoft.Storage&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; provides storage accounts, &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Microsoft.ContainerService&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; provides managed Kubernetes clusters, and so on.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Because every control plane request flows through this same path, the behavior described in this post applies regardless of which client the user is using. &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;az group deployment create&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;, a Bicep deployment from the Portal, and a direct PUT to &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;management.azure.com&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; all enter the system the same way.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2 aria-level="2"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;Synchronous and Asynchronous Operations&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Many control plane operations complete quickly enough to be handled synchronously. A GET request that reads the current state of a resource, for example, can usually return inline in tens of milliseconds. The user's client makes one request, receives one response with the requested data, and the interaction is finished.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Other operations cannot complete this way. Provisioning a managed Kubernetes cluster, deploying a multi-resource template, or tearing down a private endpoint with downstream cleanup may take seconds, minutes, or hours of actual work on the resource provider's side. There are several reasons ARM cannot just hold a synchronous HTTP connection open for the duration of this work:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="25" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559683&amp;quot;:0,&amp;quot;335559684&amp;quot;:-2,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="1" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Intermediate proxies and load balancers typically time out long-lived connections.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:0}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="25" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559683&amp;quot;:0,&amp;quot;335559684&amp;quot;:-2,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="2" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Clients may go offline (a laptop closes, a network drops) while waiting.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:0}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="25" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559683&amp;quot;:0,&amp;quot;335559684&amp;quot;:-2,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="3" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Holding a TCP connection open for an extended period consumes server resources for no useful purpose; the actual work happens elsewhere.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:0}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;To handle these cases, ARM and the resource providers implement a standard long-running operation protocol. The initial request returns immediately with a status code indicating the work has been accepted but is not yet done, along with one or more headers that tell the client where to check for status. The client then polls that status endpoint until the operation reaches a terminal state.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2 aria-level="2"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;The Long-Running Operation Protocol&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;When a resource provider receives a request that will take longer than a synchronous response can accommodate, it returns an HTTP &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;201 Created&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; or &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;202 Accepted&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; response. The response includes one or both of two key headers that direct the caller to a status endpoint.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3 aria-level="3"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 3"&gt;The &lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 3"&gt;Azure-&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 3"&gt;AsyncOperation&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 3"&gt; header&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Azure-AsyncOperation&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; contains a URL. When the client polls that URL, the response body is a structured representation of the operation's current state, including a &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;status&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; field. The status takes one of several values:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="26" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559683&amp;quot;:0,&amp;quot;335559684&amp;quot;:-2,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="1" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;An in-progress value such as &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;InProgress&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; or a resource-provider-specific equivalent.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:0}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="26" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559683&amp;quot;:0,&amp;quot;335559684&amp;quot;:-2,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="2" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;A terminal value: &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Succeeded&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;, &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Failed&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;, or &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Canceled&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:0}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;The client continues polling until the status field reaches a terminal value. Failed and canceled responses typically include an &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;error&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; field with structured detail about what went wrong.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3 aria-level="3"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 3"&gt;The &lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 3"&gt;Location&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 3"&gt; header&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Location&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; also contains a URL, but the semantics are different. While the operation is in progress, polling the Location URL returns another &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;202 Accepted&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; response, often with a refreshed &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Retry-After&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; value. Once the operation completes, polling the Location URL returns &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;200 OK&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; with a terminal payload: the resource itself for a successful create or update (a PUT), or the action's result for a POST operation such as starting, stopping, or restarting a resource. Other terminal status codes are possible depending on the operation's outcome.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Not every long-running operation returns an &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Azure-AsyncOperation&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; header; some expose only &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Location&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;. When both are present, clients should prefer the &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Azure-AsyncOperation&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; URL, because the structured status response is more informative than the implicit "still 202" signal from Location-only polling.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3 aria-level="3"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 3"&gt;The &lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 3"&gt;Retry-After&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 3"&gt; header&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Both patterns may be accompanied by a &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Retry-After&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; header, an integer giving the resource provider's suggested interval (in seconds) before the next poll. Well-behaved clients honor this value. Ignoring it and polling faster than the resource provider has asked can trigger server-side throttling, at which point the client is no better off (and often worse off) than if it had waited. When &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Retry-After&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; is absent, the client falls back to a default polling cadence determined by its own configuration.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3 aria-level="2"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;Provisioning State&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;In addition to the operation-level status returned by the LRO protocol, many Azure resources expose a &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;provisioningState&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; property in their own resource manifest. When a client issues a GET on the resource itself (not the operation status URL), the response body contains the resource's current configuration along with a &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;provisioningState&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; field.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;The provisioning state moves through a predictable lifecycle:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="27" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559683&amp;quot;:0,&amp;quot;335559684&amp;quot;:-2,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="1" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;A transitional state during work: commonly &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Creating&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;, &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Updating&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;, or &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Deleting&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;, sometimes with resource-provider-specific values.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:0}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="27" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559683&amp;quot;:0,&amp;quot;335559684&amp;quot;:-2,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="2" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;A terminal state once work completes: &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Succeeded&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;, &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Failed&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;, or &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Canceled&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:0}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Where &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;provisioningState&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; is available, clients have two distinct ways to determine completion. They can poll the async operation URL, or they can poll the resource itself and watch for &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;provisioningState&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; to reach a terminal value. The async operation URL is the authoritative signal in either case; the resource manifest's &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;provisioningState&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; is a secondary observation point that can be useful when a client is already reading the resource for other reasons.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2 aria-level="2"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;The End-to-End Polling Chain&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Putting the pieces together, the lifecycle of a long-running operation from the client's perspective looks like this:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;OL&gt;
&lt;LI aria-setsize="-1" data-leveltext="%1." data-font="" data-listid="28" data-list-defn-props="{&amp;quot;335551671&amp;quot;:1,&amp;quot;335552541&amp;quot;:0,&amp;quot;335559683&amp;quot;:0,&amp;quot;335559684&amp;quot;:-1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769242&amp;quot;:[65533,0,46],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;%1.&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="1" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;User runs a command such as &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;az aks create&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:0}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/OL&gt;
&lt;OL&gt;
&lt;LI aria-setsize="-1" data-leveltext="%1." data-font="" data-listid="28" data-list-defn-props="{&amp;quot;335551671&amp;quot;:1,&amp;quot;335552541&amp;quot;:0,&amp;quot;335559683&amp;quot;:0,&amp;quot;335559684&amp;quot;:-1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769242&amp;quot;:[65533,0,46],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;%1.&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="2" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;The CLI sends a &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;PUT&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; request to &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;management.azure.com/.../Microsoft.ContainerService/managedClusters/{name}&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:0}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/OL&gt;
&lt;OL&gt;
&lt;LI aria-setsize="-1" data-leveltext="%1." data-font="" data-listid="28" data-list-defn-props="{&amp;quot;335551671&amp;quot;:1,&amp;quot;335552541&amp;quot;:0,&amp;quot;335559683&amp;quot;:0,&amp;quot;335559684&amp;quot;:-1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769242&amp;quot;:[65533,0,46],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;%1.&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="3" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;ARM authenticates and authorizes the request, then forwards it to the &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Microsoft.ContainerService&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; resource provider.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:0}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/OL&gt;
&lt;OL&gt;
&lt;LI aria-setsize="-1" data-leveltext="%1." data-font="" data-listid="28" data-list-defn-props="{&amp;quot;335551671&amp;quot;:1,&amp;quot;335552541&amp;quot;:0,&amp;quot;335559683&amp;quot;:0,&amp;quot;335559684&amp;quot;:-1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769242&amp;quot;:[65533,0,46],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;%1.&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="4" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;The resource provider accepts the work and returns &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;202 Accepted&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; with &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Azure-AsyncOperation&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; and/or &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Location&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; headers, plus a &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Retry-After&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; value.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:0}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/OL&gt;
&lt;OL&gt;
&lt;LI aria-setsize="-1" data-leveltext="%1." data-font="" data-listid="28" data-list-defn-props="{&amp;quot;335551671&amp;quot;:1,&amp;quot;335552541&amp;quot;:0,&amp;quot;335559683&amp;quot;:0,&amp;quot;335559684&amp;quot;:-1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769242&amp;quot;:[65533,0,46],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;%1.&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="5" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;ARM forwards this response to the CLI.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:0}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/OL&gt;
&lt;OL&gt;
&lt;LI aria-setsize="-1" data-leveltext="%1." data-font="" data-listid="28" data-list-defn-props="{&amp;quot;335551671&amp;quot;:1,&amp;quot;335552541&amp;quot;:0,&amp;quot;335559683&amp;quot;:0,&amp;quot;335559684&amp;quot;:-1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769242&amp;quot;:[65533,0,46],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;%1.&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="6" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;The CLI begins polling the status URL on the suggested interval. Each poll returns the current status, in progress or terminal.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:0}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/OL&gt;
&lt;OL&gt;
&lt;LI aria-setsize="-1" data-leveltext="%1." data-font="" data-listid="28" data-list-defn-props="{&amp;quot;335551671&amp;quot;:1,&amp;quot;335552541&amp;quot;:0,&amp;quot;335559683&amp;quot;:0,&amp;quot;335559684&amp;quot;:-1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769242&amp;quot;:[65533,0,46],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;%1.&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="7" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;The resource provider continues its work in the background. Eventually the operation reaches a terminal state (&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Succeeded&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;, &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Failed&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;, or &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Canceled&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;).&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:0}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/OL&gt;
&lt;OL&gt;
&lt;LI aria-setsize="-1" data-leveltext="%1." data-font="" data-listid="28" data-list-defn-props="{&amp;quot;335551671&amp;quot;:1,&amp;quot;335552541&amp;quot;:0,&amp;quot;335559683&amp;quot;:0,&amp;quot;335559684&amp;quot;:-1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769242&amp;quot;:[65533,0,46],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;%1.&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="8" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;The next poll after that returns the terminal status along with any final response body.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:0}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/OL&gt;
&lt;OL&gt;
&lt;LI aria-setsize="-1" data-leveltext="%1." data-font="" data-listid="28" data-list-defn-props="{&amp;quot;335551671&amp;quot;:1,&amp;quot;335552541&amp;quot;:0,&amp;quot;335559683&amp;quot;:0,&amp;quot;335559684&amp;quot;:-1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769242&amp;quot;:[65533,0,46],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;%1.&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="9" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;The CLI reports completion to the user.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:0}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/OL&gt;
&lt;H2 aria-level="3"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 3"&gt;Two ways to &lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 3"&gt;observe&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 3"&gt; completion&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;The sequence above shows the client polling the async operation status URL, which is the primary and authoritative completion signal. Where a resource also exposes &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;provisioningState&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;, the client has a second option. The two differ only in what the client polls and what comes back:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="29" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559683&amp;quot;:0,&amp;quot;335559684&amp;quot;:-2,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="1" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Polling the async operation URL&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; (&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Azure-AsyncOperation&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; or &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Location&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;) returns operation-level status directly. This is the path the LRO headers point to and the one to prefer.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:0}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="29" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559683&amp;quot;:0,&amp;quot;335559684&amp;quot;:-2,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="2" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Polling the resource's &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;provisioningState&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; means issuing a GET on the resource itself and watching the &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;provisioningState&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; field reach a terminal value. This is useful when the client is already reading the resource for other reasons.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:0}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Both observe the same underlying operation. They are not different operations or different code paths on the resource provider's side; they are two different endpoints a client can watch to learn the same thing.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2 aria-level="2"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;Closing&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;The long-running operation protocol is one of those pieces of infrastructure that is invisible when it works. A user runs a command, waits, and eventually sees a result. Underneath, that simple experience rests on a well-defined contract: a &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;201&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; or &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;202&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; with a status URL, a set of headers that tell the client where and how often to check, a predictable set of terminal states, and an optional second signal through &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;provisioningState&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;. The contract is simple enough to describe in a single post and robust enough to handle everything from an eight-second deployment to a multi-hour cluster provision.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;The one part of the protocol that this post has treated as a given is the polling cadence: how often the client checks the status URL when no &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;Retry-After&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; value pins it. That cadence is more consequential than it looks. Every in-flight operation across the platform is being checked on repeatedly, and the interval between those checks determines how much work goes into useful status retrieval versus into repeatedly asking an operation that is not done yet whether it is done. Getting that cadence right, across a workload where some operations finish in seconds and others run for hours, is a genuinely interesting problem, and one worth a closer look another time.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;</description>
      <pubDate>Wed, 10 Jun 2026 02:22:06 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/apps-on-azure-blog/how-arm-tracks-work-that-takes-hours/ba-p/4526930</guid>
      <dc:creator>aravgoyal</dc:creator>
      <dc:date>2026-06-10T02:22:06Z</dc:date>
    </item>
  </channel>
</rss>

