Cloud

Azure APIM: diagnose a policy regression before backend rollback

A production runbook for qualifying an Azure API Management regression with APIM traces, policy diff, headers, cache, backend, identity, logs, validation and rollback before changing the API or target service.

27 Jul 2026 azureapi-managementapimpolicybackendheaderscacheobservabilitykqldevopsrunbookrollbackproduction

An APIM regression rarely looks like an APIM outage at first. A consumer gets a 401, 403, 429 or 502. The backend team says nothing changed. The API team sees a recent policy update, but there was also an application deployment in the same window. The easy reaction is to roll back the backend, disable a rule, broaden a header, or bypass APIM to prove that “the service works”.

The use case is an internal API exposed through Azure API Management to a private HTTP backend. A pull request changed an inbound policy: header normalization, validate-jwt, URI rewriting, response caching or rate limiting. After production rollout, only some consumers fail on one path. The runbook goal is to decide whether the incident comes from the APIM policy, backend, identity, private routing, cache or application change, then choose a bounded fix or a clean rollback.

Frame the change as an operational hypothesis

Start by writing what changed and what must not change. An APIM policy can alter the request before the backend, alter the response after the backend, reject before any backend call, or hide the real error behind a standardized response.

text apim-policy-change-scope.txt
Surface
APIM service: apim-prod-shared
API: orders-internal
Operation: POST /orders/{id}/confirm
Backend: https://orders-api.prod.internal
Changed policy section: inbound, backend, outbound or on-error
Deployment window: 2026-07-27 08:30-09:00 UTC

Hypotheses to separate
APIM rejects before backend call
APIM calls the wrong backend URL
APIM drops or rewrites a required header
APIM cache returns a stale response
Backend changed behavior independently
Private path, DNS or TLS failed
Consumer sends a payload no longer accepted

Minimum evidence
APIM trace or gateway logs with request ID
Policy diff linked to the failing operation
Backend logs with the same correlation ID
Consumer, subscription and identity observed
Replay request with controlled headers
Rollback commit or named policy version

Without this framing, the team compares unrelated symptoms. A 502 can come from an unavailable backend, but also from an overbroad set-backend-service, a private name that no longer resolves, or a certificate checked with the wrong hostname.

Tie each request to an APIM trace

The first useful evidence is the request path inside APIM. You need to know whether APIM received the call, which operation matched, which policy made a decision, whether the backend was called, and which response came back.

kusto 01-apim-request-timeline.kql
let Window = 2h;
let TargetApiId = "orders-internal";
let CorrelationId = "incident-apim-20260727-001";
ApiManagementGatewayLogs
| where TimeGenerated > ago(Window)
| where ApiId == TargetApiId
| where RequestHeaders has CorrelationId
 or BackendRequestHeaders has CorrelationId
 or ResponseHeaders has CorrelationId
| project TimeGenerated,
        ApiId,
        OperationId,
        Method,
        Url,
        ResponseCode,
        BackendUrl,
        BackendResponseCode,
        CallerIpAddress,
        SubscriptionId,
        RequestId,
        TotalTime,
        BackendTime,
        ErrorMessage
| order by TimeGenerated asc

Adapt field names to your diagnostic setup. The important point is not to conclude from the HTTP status seen by the consumer alone. If BackendResponseCode is empty, the backend may never have been called. If BackendTime is stable but the code changes, the response may be transformed in outbound or on-error policy.

Read the policy diff like production code

An APIM policy is runtime code. Review the diff at the touched operation level, not only as a full XML file.

xml policy-diff-risk-example.xml
<inbound>
<base />
<set-header name="x-tenant-id" exists-action="override">
  <value>@(context.Subscription?.Name)</value>
</set-header>
<rewrite-uri template="/v2/orders/{id}/confirm" />
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" />
<rate-limit-by-key calls="60" renewal-period="60"
  counter-key="@(context.Subscription?.Id)" />
</inbound>

Classify each line by effect.

text apim-policy-diff-review.txt
Rejects before backend
validate-jwt
check-header
ip-filter
quota, rate-limit, rate-limit-by-key

Request mutation
set-header, rewrite-uri, set-query-parameter
set-method, set-body, set-backend-service

Response mutation
set-status, set-header, set-body
return-response

Cache effect
cache-lookup, cache-store
missing or overbroad vary-by-header

Diagnostic risk
on-error replacing the message
choose branch without trace
expression depending on an optional claim or subscription

A header change can break backend authorization. A URI rewrite can call a valid but different route. A rate-limit-by-key can group too many consumers if the selected key is empty or shared.

Replay with a controlled request

The replay must preserve the details that matter: hostname, operation, APIM subscription, identity, business headers and payload. A direct backend test helps isolate the fault, but it does not replace a test through APIM.

bash 02-apim-replay-with-correlation.sh
APIM_HOST="api.internal.example.com"
SUBSCRIPTION_KEY="00000000000000000000000000000000"
TOKEN="$(cat token.jwt)"
CORRELATION_ID="incident-apim-$(date +%Y%m%d%H%M%S)"

curl -sk -o /tmp/apim-response.json -w "%{http_code}\n" "https://$APIM_HOST/orders/42/confirm" -H "ocp-apim-subscription-key: $SUBSCRIPTION_KEY" -H "authorization: Bearer $TOKEN" -H "x-correlation-id: $CORRELATION_ID" -H "x-tenant-id: tenant-a" -H "content-type: application/json" --data @confirm-order.json

echo "correlation_id=$CORRELATION_ID"

If the backend is private, keep the same logical name and the same network path as production. A call from an administration VM may use a different DNS resolver, route or identity than APIM.

Separate APIM rejection, backend failure and cache

The diagnosis should produce a readable decision. For each failed request, classify the stopping point.

text apim-failure-classification.txt
APIM rejects before backend
BackendResponseCode is empty
validate-jwt, check-header, quota or rate-limit is visible
Fix: targeted policy, expected claim, rate-limit key, operation scope

APIM calls the wrong backend
BackendUrl differs from the design
Recent rewrite-uri or set-backend-service
Fix: policy rollback or more explicit condition

Backend rejects after APIM
BackendResponseCode is 401 or 403
Header, token, mTLS or managed identity differs
Fix: restore expected header or fix backend authentication

Cache hides reality
BackendTime is null or very low
cache-lookup hit on a dynamic path
Fix: vary-by-header, vary-by-query, targeted purge, or remove cache

Private path is broken
BackendResponseCode is 502 or timeout
DNS, TLS, NSG, route or Private Endpoint to verify
Fix: network or DNS, not application policy

This classification avoids two expensive mistakes: rolling back the backend when APIM never called it, or changing APIM when the backend is rejecting a genuinely invalid identity.

Verify headers and effective identity

APIM is often where identities change shape. A consumer JWT becomes claims forwarded to the backend. A subscription key becomes an application name. A client certificate is validated, then an internal header is added. Every transformation must remain explainable.

text identity-and-header-checks.txt
Verify
Observed APIM consumer and subscription
JWT subject, audience and issuer
Claims used in policy expressions
Headers added, removed or replaced
Effective rate-limit or quota key
Client or backend certificate when mTLS is active
APIM managed identity if the backend expects Entra ID

Block recovery when
A business header is rebuilt from an optional value
Several tenants share the same rate-limit key
Backend does not log the same correlation ID
Token accepted by APIM is not the token expected by backend
A choose branch routes to an undocumented fallback backend

The goal is not to add logs everywhere under pressure. It is to prove the effective identity at call time and avoid a fix that broadens access or mixes tenants.

Decide fix, purge, rollback or no APIM change

Once the cause is classified, the response must be bounded. Disabling the whole policy or bypassing APIM is rarely the right first production action.

text apim-policy-decision.txt
Fix the policy
Touched operation identified
Minimal diff is possible
Replay reproduces the error
APIM and backend traces confirm the cause
Policy rollback remains available

Purge or temporarily disable cache
cache-lookup is proven
Response is dynamic or tenant-dependent
vary-by is insufficient
Targeted purge is possible

Roll back the policy
Several mutations were mixed
Legitimate production requests are rejected
Trace is incomplete or side effect is not bounded
Previous version is known and deployable

Do not touch APIM
APIM calls the right backend
Headers and identity are compliant
BackendResponseCode proves an application regression
Backend rollback or service fix should be prioritized

The healthy rule is simple: change only the layer that produced the evidence. If evidence is missing, return to the safest known state instead of stacking exceptions.

Validate after action

After a fix or rollback, replay the original scenario and one negative control. An API that works again but remains unobservable is still fragile.

text apim-post-action-validation.txt
Functional validation
Initial request replayed through APIM
Backend called with the right hostname
Correlation ID visible in APIM and backend
Expected headers present, forbidden headers absent
No increase in 401, 403, 429 or 502 on the operation

Control validation
Invalid JWT rejected before backend
Unauthorized tenant refused cleanly
Rate limit bounded by expected consumer
Cache does not answer tenant-dependent data
on-error keeps a diagnostic identifier

Rollback ready
Previous policy commit or revision identified
Restore command known
Monitoring window defined
Ticket keeps diff, traces and decision

If the replay succeeds but the trace still cannot follow the request, recovery is not complete. The next regression will depend on the same assumptions.

Conclusion

An APIM regression should be treated as a contract failure between consumer, gateway and backend. Good diagnosis separates pre-backend rejection, request mutation, cache, identity, private path and application behavior.

The useful decision is not simply “rollback or not”. It is: fix a minimal policy, purge a cache, restore a known version, or leave APIM untouched because the backend is the faulty layer. That choice should stand on correlated traces, reproducible replay and rollback readiness.