Loading Now

How ACR Artifact Cache Handles Multi-Arch Images: What Gets Cached and When Webhooks Fire

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)

When it comes to using Azure Container Registry (ACR) with the Artifact Cache and Webhooks, we often hear a few recurring questions from our users. Here are the three most common ones:

  1. “What actually gets cached when I pull a multi-architecture (multi-arch) image through Artifact Cache?” — Does ACR cache all architectures or just the one I’ve requested?
  2. “How can I find out when an image has been cached using Webhooks?” — Particularly for multi-arch images, how do I know when the image is fully stored in my local ACR, and pulls are no longer going to the upstream?
  3. “When do storage fees begin for cached images?” — How can I tell when an image has been stored locally and when charges for cached images start applying?

In this article, we’ll clarify how these processes work and guide you step-by-step to check it out in your own environment using ACR’s artifact cache and webhooks.

  • When you pull a multi-arch image via the Artifact Cache, ACR begins by immediately proxying the pull to the upstream source. It then carries out an asynchronous task to copy the manifest list (which contains references for all platforms) and only the manifest for the platform you’ve pulled into local storage. The other architecture manifests will only get copied when someone specifically requests them.
  • Webhooks in ACR trigger once the asynchronous copying is completed—indicating that the image is now stored in the local ACR and any further pulls won’t be going upstream. For a single-platform pull of a multi-arch image, you’ll see three push webhook events: two for the manifest list (one tagged and one untagged) and one for the platform-specific manifest.
  • No webhooks are triggered for blob or layer copies—just a single webhook event occurs for each digest and tag that have been copied asynchronously and stored locally.

The ACR Artifact Cache serves as a pull-through caching feature. You start by setting up a cache rule that connects an upstream repository (like Docker Hub or Microsoft Artifact Registry) to a local ACR repository.

Here’s a brief rundown of how this works when an image hasn’t been cached yet:

  1. A client initiates a pull from ACR (for example, `docker pull myacr.azurecr.io/nginx:latest`).
  2. ACR doesn’t redirect the client to the upstream registry. Instead, ACR will pull the image through on behalf of the client—acting as a proxy and streaming the content back to the client. The client only interacts with the downstream ACR that has been set up in the cache rule.
  3. Alongside that, ACR starts an asynchronous job to copy the image into its own storage.
  4. Until this process is complete, future pulls for that same image will still be routed upstream.
  5. Once the asynchronous copy is done, the image is saved in the downstream ACR. From then on, every pull is served directly from the local ACR without needing to access the upstream.

This is a crucial point: the Artifact Cache acts as a pull-through proxy, not a redirect. Clients will always connect with your ACR endpoint and never have direct communication with upstream. The caching takes place asynchronously in the background after the initial pull.

If you’d like to dive deeper, check the Artifact Cache documentation.

Multi-architecture images use a manifest list (sometimes called an OCI image index) that includes references to various platform-specific manifests. In our test, we used Docker manifest list media types (application/vnd.docker.distribution.manifest.list.v2+json). It’s worth noting that while OCI image indexes have similar, distinct media types, we focus on Docker media types here.

For instance, the “mcr.microsoft.com/cbl-mariner/base/core:2.0” multi-arch image from Microsoft Artifact Registry features a manifest list pointing to two specific platform images, each with their own digest:

ArchitectureDigest
linux/amd64sha256:fdf30afe7338…
linux/arm64sha256:c981b0618917…

When a Docker client or containerd pulls this image, it processes the manifest list and downloads just the one that matches its architecture.

In the event of a multi-arch pull through the Artifact Cache, there are three potential scenarios:

  • (A) Save only the single platform manifest for the requesting architecture?
  • (B) Save the manifest list along with the client’s architecture only?
  • (C) Save both the manifest list and all architectures?

The correct answer is (B), and we’ll demonstrate through an experiment how to detect when the images are cached using webhooks.

To make this clearer, let’s work through a step-by-step example utilizing ACR webhooks to capture each push event generated during a cache population. You can replicate this in your own setup.

  • You need an ACR registry (any SKU — Basic, Standard, or Premium).
  • Ensure you have Azure CLI (az) installed and you’re logged in.
  • Install Docker Desktop (with Docker CLI) or Podman Desktop (with Podman CLI).

For receiving and displaying webhook payloads, you’ll need an HTTP endpoint. You can utilize webhook.site for a free temporary endpoint—just visit the site and copy your unique URL.

Next, set up an ACR push webhook that only triggers upon push events for your testing repository. When tags and digests are copied asynchronously to your downstream ACR during artifact cache operations, they count as push events, making this webhook an effective way to observe and validate cache-driven push behavior in the registry. Limiting the webhook to push events helps prevent interference from other activities in the registry during this test.

az acr webhook create \
  --registry  \
  --name cachepushtest \
  --uri  \
  --actions push \
  --scope "test/cbl-mariner/base/core:*" \
  --status enabled

Next, send a ping to ensure the webhook endpoint is accessible:

az acr webhook ping \
  --registry  \
  --name cachepushtest

Check your webhook.site page—you should see a POST request with “action”: “ping”.

Now, map the upstream Microsoft Artifact Registry repository to a local test namespace. Microsoft Artifact Registry doesn’t need any credentials, which helps streamline setup.

az acr cache create \
  --registry  \
  --name cblmariner-cache-test \
  --source-repo mcr.microsoft.com/cbl-mariner/base/core \
  --target-repo test/cbl-mariner/base/core

Log into your registry and pull the multi-arch image. Make sure to use `–platform` for a consistent test.

Note: If the image is already in the downstream ACR’s cache, pull requests result in cache hits served from local storage, completely bypassing the initial asynchronous process and not generating new webhook events. To trigger webhook events again, you’ll need to remove cached image tags and digests from the downstream ACR, clearing the cache for the next pull.

az acr login --name 

docker pull --platform linux/amd64 \

.azurecr.io/test/cbl-mariner/base/core:2.0

Once you’ve waited for the webhook delivery to finish, check the events:

az acr webhook list-events \
  --registry  \
  --name cachepushtest

You can also check webhook.site for raw payloads.

Excluding the initial webhook test ping, you’ll observe 3 ACR webhook events when running “docker pull –platform linux/amd64 .azurecr.io/test/cbl-mariner/base/core:2.0” configured with a cache rule against the upstream “mcr.microsoft.com/cbl-mariner/base/core:2.0” multi-arch image from Microsoft Artifact Registry:

{
  "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
  }
}

{
  "action": "push",
  "target": {
    "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
    "digest": "sha256:c833841d2dcfd3081d2ee807050d19368854f70d9b6faef027463e2c6f45ee41",
    "repository": "test/cbl-mariner/base/core",
    "size": 860
  }
}

This event shares a digest with Event 1, but it lacks a tag. ACR generated two push webhook events for the same manifest-list digest: one has a tag, the other does not.

{
  "action": "push",
  "target": {
    "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
    "digest": "sha256:fdf30afe733831d3af0db95aa8e6870fb1094b2c4f531caaaa06e37481b95253",
    "repository": "test/cbl-mariner/base/core",
    "size": 736
  }
}

This represents the linux/amd64 platform-specific manifest.

  • No webhook for linux/arm64 (sha256:c981b061…). Despite being referenced in the manifest list, the arm64 platform manifest wasn’t cached since the pull operation requested only the linux/amd64 image using the `–platform` flag.
  • No ACR push webhooks fired for blob/layer copies—only a single push webhook event triggers for each digest and tag that is copied and stored asynchronously.

To verify what ACR actually stored, run the following command:

az acr manifest list-metadata \
  --registry  \
  --name test/cbl-mariner/base/core \
  -o table
DigestMediaTypeArchitecture
sha256:c833841d…manifest.list.v2+json(multi-arch index)
sha256:fdf30afe…manifest.v2+jsonamd64

Only two manifests were stored: the manifest list and the amd64 manifest. The manifest list does reference both linux/amd64 and linux/arm64, but the arm64 manifest was not downloaded.

The ACR Artifact Cache performs a partial closure copy:

  1. The complete manifest list is cached. This ensures that the multi-arch index remains intact so that clients can request any platform and receive a valid manifest list response. The manifest list still points to all platforms.
  2. Only the platform manifest you requested is cached. If you pull linux/amd64, only the amd64 manifest and its layers make it to ACR storage. The arm64 manifest stays cached upstream—if someone requests it, that will pull from upstream, kicking off another async copy.
  3. Pull requests for additional architectures lead to further caching. For example, if another client later pulls –platform linux/arm64, ACR will pull that from upstream and start another async copy. More webhook events will trigger for the arm64 manifest once that’s completed.
EventMediaTypeTaggedCount
Manifest list pushmanifest.list.v2+jsonYes (2.0)1
Manifest list pushmanifest.list.v2+jsonNo1
Platform manifest pushmanifest.v2+jsonNo1 per architecture pulled

In total for a single-platform pull, you’ll see 3 ACR push webhook events.

When a client requests an uncached image, ACR will pull the content from upstream and serve it right away—but it won’t yet be stored in ACR. An asynchronous copy will run in the background for local storage. Until it’s finished, subsequent pulls of that image will still be routed upstream.

The webhook push event activates once the async copy is complete, confirming that the image is now saved in ACR. After that point, pulls will be served directly from ACR, eliminating any upstream traffic. This is what the webhook indicates—not just that a pull occurred, but that the image is now locally cached in your registry.

Additionally, the webhook shows when an image is stored locally and when storage fees for that cached image start accruing.

There are two levels of completion to keep in mind:

  • Locally cached tagged: A push event indicating a mediaType of application/vnd.docker.distribution.manifest.list.v2+json and a non-null tag field indicates the manifest list is stored in ACR. This tag now directs locally, and storage fees start for the manifest list artifact.
  • Locally cached specific platform: A push event with mediaType of application/vnd.docker.distribution.manifest.v2+json means that a platform-specific manifest (and its layers) are stored locally. Pulls for that architecture are now entirely served from ACR, and storage fees begin for the platform-specific artifact.

To determine which platform a manifest push corresponds to, pre-calculate 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

Keep track of the architecture-to-digest mapping (e.g., linux/amd64 → sha256:fdf30afe…). In your webhook handler, match incoming digest values against this mapping to figure out which architecture was cached.

Note: Webhook events may not arrive sequentially, and retries can lead to duplicate deliveries. To avoid issues, deduplicate events by id or digest in your handler and don’t assume the ordering between manifest-list and platform-manifest events.

After finishing the experiment, ensure you clean up the test resources:

az acr webhook delete --registry  --name cachepushtest
az acr cache delete --registry  --name cblmariner-cache-test --yes
az acr repository delete --name  --repository test/cbl-mariner/base/core --yes
QuestionAnswer
What gets cached for multi-arch images?Both the full manifest list and just the requested platform manifest
Are other architectures cached?No—they’re only cached on demand when requested
How many webhooks are triggered?Three for a single-platform pull (two for the manifest list, one for the platform manifest)
Do blob/layer uploads trigger webhooks?No—only manifest pushes do
How can I tell when an image is locally cached?Watch for push webhooks—they indicate when the async copy is complete and when pulls are no longer directed to upstream

If you have more questions about the Artifact Cache or webhook functionality, please reach out via the Azure Container Registry GitHub repository.

Share this content:


Discover more from Qureshi

Subscribe to get the latest posts sent to your email.

Discover more from Qureshi

Subscribe now to keep reading and get access to the full archive.

Continue reading