• English icon
  • German icon
Explainer

Azure says my Container App is Healthy. Why is it broken?

· Series: Containers on Azure

azureazure-container-appshealth-probescontainersdevops

I removed one setting from a container app. Azure Container Apps built a new revision, marked it Healthy and gave it all the traffic. The app answered model: not-configured. No failed deployment, no error, everything green, and the app was broken.

This explainer shows what "Healthy" really checks, why a wrong ingress port skips every check, and the readiness probe that keeps a broken release away from users. Everything below comes from a real run in my own Azure subscription.

Key points

  • Healthy means the probes pass. Without probes of your own, Container Apps adds default TCP probes on the ingress target port. They only ask whether something accepts a connection.
  • Ingress is not part of a revision. The target port, traffic rules and secrets are app-level configuration: a change creates no revision and goes live for every revision at once, with no gate in between.
  • Readiness decides traffic, liveness restarts. Put what a replica needs to do its job into readiness; keep liveness to the process itself, and never put the database into it.
  • Debug from the outside in. Revision list, then console and system logs, then Log Analytics for history; deactivate a bad revision instead of deleting it.

What Healthy checks

Removing an environment variable changes the revision's template, so Container Apps builds a new revision. A revision is a frozen snapshot of your container settings: the image, the environment variables, the scale rules. It runs as one or more replicas, the running copies.

In single revision mode (the default), the old revision keeps all the traffic until the new one is ready: provisioned, scaled up, and past its startup and readiness probes. That is the gate. I had defined no probes, so with MODEL_NAME gone, the new revision still got 100 % of the traffic.

The reason: with ingress enabled, Container Apps adds default probes for every type you don't define. They are TCP checks on the ingress target port:

ProbeDefault check
StartupTCP on the target port, every second, up to 240 failures
ReadinessTCP on the target port, every 5 seconds
LivenessTCP on the target port

The app reads the setting with a fallback, so it started, the port opened, and the gate let it through:

MODEL_NAME = os.getenv("MODEL_NAME", "not-configured")

To find a change like that, compare the last good revision with the new one:

az containerapp revision show -n ai-api -g rg-demo \
--revision ai-api--v2 \
--query "properties.template.containers[0].env" -o table

For v1 this printed MODEL_NAME gpt-5.4-mini. For v2 it printed nothing.

The wrong port

The app listens on port 8000. I changed the ingress target port to 3000:

az containerapp ingress update -n ai-api -g rg-demo \
--target-port 3000

A wrong port sounds like a failed deployment, but there is no new revision and no check. Ingress is application-scope configuration, like traffic rules and secrets: it sits in front of every revision and changes all of them at once. The very next request got a 503 with Connection refused.

The console log shows the mismatch in one line: gunicorn is Listening at: http://0.0.0.0:8000, while ingress sends to 3000.

A restart doesn't help. The new replica's default startup probe now knocks on port 3000 and fails every second (Probe of StartUp failed with status code: 1 in the system log). The revision list still said Healthy, because the old replica kept running, and every request still failed.

Readiness and liveness

You make Healthy mean something by giving the gate better questions:

ProbeQuestionWhen it fails
ReadinessCan this replica take requests right now?the replica gets no traffic, but keeps running
LivenessIs the process still alive?Container Apps restarts the replica
StartupHas the app started?runs only while the app starts; then readiness and liveness take over

Readiness gets its own endpoint. This one returns 503 while MODEL_NAME is missing:

@app.route("/health/ready")
def ready():
if MODEL_NAME == "not-configured":
return {"status": "not ready"}, 503
return {"status": "ready"}

And the probes in the container template (HTTP probes count a status from 200 to 399 as success):

probes:
- type: Readiness
httpGet: { path: /health/ready, port: 8000 }
periodSeconds: 5
- type: Liveness
httpGet: { path: /health, port: 8000 }
periodSeconds: 10

The trap: if your liveness probe also checks the database, and the database is down for a minute, every replica restarts, again and again. A short blip becomes an outage, with cold starts on top. Put the database check into readiness instead: during the outage the replicas stop taking traffic, nothing restarts, and they come back by themselves when the database does.

When it breaks anyway

  1. The revision list: which revision is active, is it healthy, where does the traffic go.
  2. The logs: console logs are what your app prints; system logs are what the platform saw (probes, image pulls, restarts). az containerapp logs show streams both (--type system; system logs only come as JSON).
  3. History in Log Analytics: the environment's workspace keeps them, a few minutes behind.
  4. Compare the failing revision with the last good one.
  5. Deactivate, don't delete: it stops the replicas, inactive revisions cost nothing, and the evidence stays.

A restart is not a fix: it starts the same image with the same configuration.

az containerapp revision list -n ai-api -g rg-demo -o table
az containerapp logs show -n ai-api -g rg-demo --type system
az containerapp revision deactivate -n ai-api -g rg-demo \
--revision ai-api--v5

What happened when I ran it

Tested on October 19, 2026 in my own Azure subscription (westeurope, Azure CLI 2.88.0); everything was deleted afterwards. The app is the Flask API from the Microsoft Learn exercise, plus the /health/ready endpoint above. My first attempt in germanywestcentral failed at the environment: AKSCapacityHeavyUsage, "Creating a new cluster is unavailable at this time in region germanywestcentral". In westeurope the environment took 18.6 minutes, the cloud build 69 seconds, the app 43 seconds.

The broken revision, live:

> az containerapp revision list -n ai-api -g rg-demo --all -o table
CreatedTime Active Replicas TrafficWeight HealthState ProvisioningState Name
------------------------- -------- ---------- --------------- ------------- ------------------- ---------------
2026-10-19T15:32:45+00:00 False 0 0 Healthy Provisioned ai-api--f5uwknl
2026-10-19T15:33:07+00:00 True 1 0 Healthy Provisioned ai-api--v1
2026-10-19T15:34:06+00:00 True 1 100 Healthy Provisioned ai-api--v2
> (irm https://$FQDN/).model
name
----
not-configured

Each az containerapp update took 18 to 22 seconds and returned before the new revision was ready: asked for the latest ready revision, it still printed the old one.

The wrong port:

> curl.exe -sS -i -m 30 https://$FQDN/health
HTTP/1.1 503 Service Unavailable
upstream connect error or disconnect/reset before headers. retried and the latest
reset reason: remote connection failure, transport failure reason: delayed connect
error: Connection refused

The same mistake with the readiness probe (revision v5, four minutes later): v5 never became ready, the app kept answering gpt-5.4-mini from v4, and the system log showed the gate at work:

Probe of Readiness failed with status code: 503
Probe of Readiness failed with status code: 503
Probe of Readiness failed with status code: 503

Log Analytics, queried two and a half minutes after the last call:

RevisionName_s TableName Calls Failed
---------------- ------------- ------- --------
ai-api--v4 PrimaryResult 70 0
ai-api--v5 PrimaryResult 53 53

(The first row of the revision list, ai-api--f5uwknl, is a placeholder revision the CLI created while it set up the managed identity for the image pull.)

One more detail: in single revision mode, revision list showed v5 with a traffic weight of 100 while it was not ready. Requests still went to v4, the latest ready revision, and the first one took 24 seconds, because v4 had scaled to zero in the meantime.

Left out of the video

Tags vs digests for production images, start, stop and scale to zero, CPU and memory sizing (on Consumption from 0.25 vCPU / 0.5 GiB up to 2 vCPU / 4 GiB per replica), structured logging fields and multiple revision mode with traffic splitting. They are in the Microsoft Learn module below.

Further reading

Video transcript

I removed one setting from a container app. Azure built a new revision, marked it Healthy, and gave it all the traffic. And the app answered: model, not configured. No failed deployment. No error. Everything green, and the app was broken.

In this video, you'll see what Healthy really means in Container Apps, and why it missed both mistakes I made on purpose. And at the end, the same broken release again, with one probe added.

What does Healthy actually check? Most people read Healthy as: the app works. So let's test that. Removing an environment variable changes the revision's template, so Container Apps builds a new revision: v2. A revision is a frozen snapshot of your container settings: the image, the environment variables, the scale rules. It runs as one or more replicas, the running copies. Before v2 gets any traffic, it has to pass a gate: its startup and readiness probes. I never defined any. So, with MODEL_NAME gone, does v2 get the traffic? All of it. Healthy, one hundred percent. v1 kept running, with zero traffic. Here's why. Without probes of your own, Container Apps adds default ones: TCP checks on the target port. They ask exactly one question: does something accept a connection on port 8000? And the code reads MODEL_NAME with a fallback: not configured. So the app started, the port opened, and the gate let it through. To find a change like that, compare the last good revision with the new one. v1 has MODEL_NAME. The list for v2 is empty. So Healthy means the probes pass. And the default probe only checks that the door opens, not what happens behind it.

What if the port itself is wrong? The app listens on port 8000. I changed the ingress target port to 3000. A wrong port sounds like a failed deployment. So, does Container Apps build a new revision and test it first? No new revision. No probe, no gate. The very next request got a 503: connection refused. Ingress isn't part of a revision. The target port, traffic rules and secrets are app-level configuration. That configuration sits in front of every revision, and a change goes live for all of them at once. There is no gate in between. The console log shows the mismatch in one line: the app is listening at port 8000, and ingress sends to 3000. And a restart doesn't help. The new replica's default startup probe now knocks on port 3000, and fails, every second. The revision list still said Healthy, because the old replica kept running. And every request still failed. This series runs one Azure service at a time, for real. Subscribe to follow the series.

So how do you make Healthy mean something? You give the gate better questions. Two probes matter here. Readiness asks: can this replica take requests right now? If it fails, the replica gets no traffic, but it keeps running. Liveness asks: is the process still alive? If it fails, Container Apps restarts the replica. Readiness gets its own endpoint. Ours returns a 503 while MODEL_NAME is missing, and 200 when the replica can do its job. In the container's YAML, the readiness probe calls that path every five seconds. Liveness calls the plain health path. Now a trap. Say your liveness probe also checks the database, and the database is down for a minute. What happens to your replicas? They all restart, again and again. A one-minute blip becomes an outage, with cold starts on top. So readiness checks what this replica needs to do its job. Liveness only checks the process itself. Put the database check into readiness instead. During an outage, the replicas stop taking traffic, nothing restarts, and they come back by themselves when the database does. And the startup probe only runs while the app starts. Once it passes, readiness and liveness take over.

When it breaks anyway, where do you look first? Not inside the container. Start with the revision list: which revision is active, is it healthy, and where does the traffic go. Then the logs. Console logs are what your app prints. System logs are what the platform saw: probes, image pulls, restarts. The command az containerapp logs show streams both, live. For history, query Log Analytics, the workspace where the environment keeps its logs. When I queried it two and a half minutes later, every probe call was there. And deactivate a bad revision instead of deleting it. That stops its replicas, it costs nothing, and it keeps the evidence. One more: a restart is not a fix. It starts the same image with the same configuration, as the wrong port showed.

To sum it up. Healthy means the probes pass, and the default probes only check the port. Ingress changes skip the gate, so compare the target port with the logs. Readiness decides traffic, liveness restarts, so keep dependencies out of liveness. And start from the revision list and the logs, not from a shell.

And the broken release from the start, now with that readiness probe. I removed MODEL_NAME again. Revision v5 started, and asked its readiness endpoint every five seconds. 503, every time. It never became ready. Requests stayed on v4, and kept answering: GPT 5.4 mini. Log Analytics counted it later: fifty-three readiness checks on v5, fifty-three failed. The same mistake, and no user saw it.

The video on screen now shows how revisions, secrets and registry pulls work when you deploy to Container Apps. If this was useful, subscribe for more practical .NET and Azure.

Need this in your own Azure environment?

I build, migrate and run .NET platforms on Azure, with Azure DevOps and Terraform. If your team wants this set up properly, see what I offer.

See services