Clarifying Azure Container Registry's Artifact Cache behavior for multi-architecture container images, and how to use Webhooks to detect when an image is fully cached locally and no longer being pulled through from upstream.
By Johnson Shi (Senior Product Manager), Toddy Mladenov (Principal PM Manager), Luis Dieguez (Principal SWE Manager), Akash Singhal (Senior Software Engineer), Kiran Challa (Senior Software Engineer), Ren Shao (Senior Software Engineer)
Introduction
Three of the most common questions we hear from teams using Azure Container Registry (ACR) Artifact Cache and Webhooks are:
- "When I pull a multi-architecture (multi-arch) image through Artifact Cache, what exactly gets cached?" — Does ACR pull all architectures, or just the one I requested?
- "How do I know when an image has been cached using Webhooks?" — Especially for multi-arch images, how can I tell when the image is fully stored locally in a cache rule's downstream ACR and pulls are no longer being proxied (pulled through) to upstream?
- "When do storage charges begin applying for cached images?" — How can I determine when an image has been stored locally and when storage charges for the cached image begin incurring?
In this post, we clarify the exact behavior and provide a step-by-step walkthrough so you can verify it in your own environment using ACR artifact cache and webhooks.
Key Takeaways
- When you pull a multi-arch image through Artifact Cache, ACR proxies the pull to upstream immediately. In the background, ACR asynchronously copies the manifest list artifact (referencing all platforms) and only the platform manifest artifact that was pulled into local storage. Other architecture manifests referenced by the manifest list artifact are not copied until someone pulls them.
- ACR push webhooks fire when the async copy completes for the artifact cache feature — signaling that the image is now stored locally in a cache rule's downstream ACR and subsequent pulls will no longer be proxied to upstream. For a single-platform pull of a multi-arch image, ACR fires 3 push webhook events: two for the manifest list (one tagged, one untagged) and one for the platform-specific manifest.
- No ACR push webhooks fire for blob/layer copies — only a single push webhook event is triggered per digest and tag that is asynchronously copied and locally stored.
Background
What Is Artifact Cache?
ACR Artifact Cache is a pull-through caching feature in Azure Container Registry. You define a cache rule that maps an upstream repository (such as Docker Hub or Microsoft Artifact Registry) to a local ACR repository.
Here's how it works when an image is not yet cached:
- A client pulls from ACR (e.g., docker pull myacr.azurecr.io/nginx:latest).
- ACR does not redirect the client to the upstream registry. Instead, ACR pulls the image through on the client's behalf — ACR proxies the request to upstream and streams the content back to the client. The client only ever talks to the downstream ACR in a cache rule.
- At the same time, ACR kicks off an asynchronous job to copy the image into the downstream ACR's own storage.
- Until that async copy completes, subsequent pulls of the same image are still pulled through to upstream.
- Once the async copy finishes, the image is stored locally in the downstream ACR. From that point on, all pulls are served directly from the downstream ACR with no upstream traffic.
This is an important distinction: Artifact Cache is a pull-through proxy, not a redirect. Clients always interact with your ACR endpoint, never directly with upstream. The caching happens asynchronously in the background after the first pull.
For more details, see the Artifact Cache documentation.
Multi-Arch Images
Multi-architecture images use a manifest list (also called an OCI image index) that references multiple platform-specific manifests. In this experiment, we used Docker manifest list media types (application/vnd.docker.distribution.manifest.list.v2+json). OCI image indexes use equivalent but distinct media types — the caching behavior is expected to be the same, but our observations are specific to Docker media types.
For example, the "mcr.microsoft.com/cbl-mariner/base/core:2.0" multi-arch image from Microsoft Artifact Registry is a manifest list that references two platform-specific images, each with their own digest:
| Architecture | Digest |
|---|---|
| linux/amd64 | sha256:fdf30afe7338... |
| linux/arm64 | sha256:c981b0618917... |
When a Docker client or containerd pulls this image, it resolves the manifest list and downloads only the manifest matching its platform.
The Behavior in Question
When Artifact Cache handles a multi-arch pull, there are three possible outcomes:
- (A) Cache only the single platform manifest for the client's architecture?
- (B) Cache the manifest list plus the client's architecture only?
- (C) Cache the manifest list plus all architectures?
And how many ACR push webhook events does each produce? The answer is (B), and we demonstrate an experiment to detect when the images are cached using webhooks.
Walkthrough: Observing Cache Behavior with Webhooks
To illustrate this behavior, we walk through a step-by-step example using ACR webhooks to capture every push event that ACR fires during a cache population. You can follow along in your own environment.
Prerequisites
- An ACR registry (any SKU — Basic, Standard, or Premium)
- Azure CLI (az) installed and logged in
- Docker Desktop (with Docker CLI) or Podman Desktop (with Podman CLI) installed
Step 1: Set Up a Webhook Endpoint
You need an HTTP endpoint that can receive and display webhook payloads. webhook.site provides a free temporary endpoint — visit the site and copy your unique URL.
Step 2: Create a Scoped Webhook
Create an ACR push webhook that only fires for push events on the specific repository you'll use for testing. When tags and digests are copied asynchronously into a downstream ACR during artifact cache operations, they are classified as push events on the downstream ACR. As such, the push webhook should fire for these operations, making push webhooks a useful way to observe and validate cache-driven push behavior in the registry. Scoping the webhook to push events also eliminates noise from other registry activity during this experiment.
az acr webhook create \
--registry <your-registry> \
--name cachepushtest \
--uri <your-webhook-site-url> \
--actions push \
--scope "test/cbl-mariner/base/core:*" \
--status enabled
Step 3: Verify the Webhook
Send a ping to confirm the webhook endpoint is reachable:
az acr webhook ping \
--registry <your-registry> \
--name cachepushtest
Check your webhook.site page — you should see a POST request with "action": "ping".
Step 4: Create a Cache Rule
Map the upstream Microsoft Artifact Registry repository to a local test namespace. Microsoft Artifact Registry doesn't require credentials, which simplifies setup.
az acr cache create \
--registry <your-registry> \
--name cblmariner-cache-test \
--source-repo mcr.microsoft.com/cbl-mariner/base/core \
--target-repo test/cbl-mariner/base/core
Step 5: Pull Through Cache
Log in to your registry and pull the multi-arch image. Use --platform to make the test reproducible.
Note: When the image already exists in the downstream ACR's cache, pull requests result in cache hits served directly from local storage, bypassing the initial asynchronous copy process and generating no new webhook events. To force webhook events to fire again, delete the cached image tags and digests from the downstream ACR. This clears the cache and triggers the asynchronous copy behavior on the next pull.
az acr login --name <your-registry>
docker pull --platform linux/amd64 \
<your-registry>.azurecr.io/test/cbl-mariner/base/core:2.0
Step 6: Observe the Webhook Events
Wait for webhook delivery to complete, then inspect the events:
az acr webhook list-events \
--registry <your-registry> \
--name cachepushtest
You can also check webhook.site to see the raw payloads.
Results
Excluding the initial webhook test ping, you will observe 3 ACR webhook events when running "docker pull --platform linux/amd64 <your-registry>.azurecr.io/test/cbl-mariner/base/core:2.0" that is configured with a cache rule against the upstream "mcr.microsoft.com/cbl-mariner/base/core:2.0" multi-arch image from Microsoft Artifact Registry:
Event 1: Manifest List (Tagged)
{
"action": "push",
"target": {
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"digest": "sha256:c833841d2dcfd3081d2ee807050d19368854f70d9b6faef027463e2c6f45ee41",
"repository": "test/cbl-mariner/base/core",
"tag": "2.0",
"size": 860
}
}
Event 2: Manifest List (Untagged)
{
"action": "push",
"target": {
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"digest": "sha256:c833841d2dcfd3081d2ee807050d19368854f70d9b6faef027463e2c6f45ee41",
"repository": "test/cbl-mariner/base/core",
"size": 860
}
}
Same digest as Event 1, but without a tag. ACR emitted two push webhook events for the same manifest-list digest: one tagged, one untagged.
Event 3: Platform Manifest (amd64)
{
"action": "push",
"target": {
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:fdf30afe733831d3af0db95aa8e6870fb1094b2c4f531caaaa06e37481b95253",
"repository": "test/cbl-mariner/base/core",
"size": 736
}
}
This is the linux/amd64 platform-specific manifest.
What Was Not Observed
- No webhook for linux/arm64 (sha256:c981b061...). Even though the manifest list references it, the arm64 platform manifest was not cached because the docker pull operation specifically pulled only the linux/amd64 image using the --platform flag.
- No ACR push webhooks fire for blob/layer copies — only a single push webhook event is triggered per digest and tag that is asynchronously copied and locally stored.
Cross-Referencing with ACR Manifests
We confirmed the webhook results by listing what ACR actually stored:
az acr manifest list-metadata \
--registry <your-registry> \
--name test/cbl-mariner/base/core \
-o table
| Digest | MediaType | Architecture |
|---|---|---|
| sha256:c833841d... | manifest.list.v2+json | (multi-arch index) |
| sha256:fdf30afe... | manifest.v2+json | amd64 |
Only two manifests were stored: the manifest list and the amd64 manifest. The manifest list itself still references both linux/amd64 and linux/arm64, but the arm64 manifest was not downloaded.
Findings
Artifact Cache Multi-Arch Behavior: Caches Manifest List + Requested Architecture Only
ACR Artifact Cache performs a partial closure copy:
- The full manifest list is cached. This preserves the multi-arch index so that clients requesting any platform will get a valid manifest list response. The manifest list still references all platforms.
- Only the requested platform manifest is cached. If you pull linux/amd64, only the amd64 manifest and its layers are copied into ACR storage. The arm64 manifest remains uncached — if someone pulls it, it will be pulled through from upstream and a new async copy job will start.
- Subsequent architecture pulls trigger additional caching. If a different client later pulls --platform linux/arm64, ACR will pull through from upstream for that architecture and kick off another async copy. Once complete, additional webhook events will fire for the arm64 manifest.
Webhook Behavior Summary
| Event | MediaType | Tagged | Count |
|---|---|---|---|
| Manifest list push | manifest.list.v2+json | Yes (2.0) | 1 |
| Manifest list push | manifest.list.v2+json | No | 1 |
| Platform manifest push | manifest.v2+json | No | 1 per architecture pulled |
Total for a single-platform pull: 3 ACR push webhook events.
Using Webhooks to Know When an Image Is Locally Cached
When a client pulls an uncached image, ACR pulls the content through from upstream and serves it immediately — but the image is not yet stored in ACR. An asynchronous copy job runs in the background to store the image locally. Until that job completes, subsequent pulls of the same image continue to be pulled through to upstream.
The webhook push event fires when the async copy completes and the image is actually stored in ACR. After this point, pulls are served directly from ACR with no upstream traffic. This is what the webhook signals — not that a pull happened, but that the image is now locally cached in your registry.
The webhook also signals when the image is stored locally and when storage charges for the cached image begin to apply.
There are two levels of completion to consider:
- Tag locally cached: A push event with mediaType of application/vnd.docker.distribution.manifest.list.v2+json and a non-null tag field means the manifest list is stored in ACR. The tag now resolves locally. Storage charges are now incurred for the manifest list artifact.
- Specific platform locally cached: A push event with mediaType of application/vnd.docker.distribution.manifest.v2+json means a platform-specific manifest (and its layers) are stored locally. Pulls for that architecture are now served entirely from ACR. Storage charges are now incurred for the platform-specific artifact
To determine which platform a manifest push corresponds to, pre-compute the per-platform digests from the upstream manifest list:
# Get the upstream manifest list with per-platform digests
docker manifest inspect mcr.microsoft.com/cbl-mariner/base/core:2.0
Take note of the architecture-to-digest mapping (e.g., linux/amd64 → sha256:fdf30afe...). In your webhook handler, match incoming digest values against this mapping to identify which architecture was cached.
Note: Webhook events may not arrive in strict order, and retries can produce duplicate deliveries. Deduplicate events by id or digest in your handler, and don't assume ordering between the manifest-list and platform-manifest events.
Cleanup
After running the experiment, clean up the test resources:
az acr webhook delete --registry <your-registry> --name cachepushtest
az acr cache delete --registry <your-registry> --name cblmariner-cache-test --yes
az acr repository delete --name <your-registry> --repository test/cbl-mariner/base/core --yes
Summary
| Question | Answer |
|---|---|
| What gets cached for multi-arch images? | The full manifest list + only the requested platform manifest |
| Are other architectures cached? | No — only on demand when someone pulls them |
| How many webhooks fire? | 3 for a single-platform pull (2 for manifest list, 1 for platform manifest) |
| Do blob/layer uploads fire webhooks? | No — only manifest pushes |
| How do I know when an image is locally cached? | Listen for push webhooks — they fire when the async copy completes and pulls no longer go to upstream |
If you have further questions about Artifact Cache or webhook behavior, reach out to us on the Azure Container Registry GitHub repository.