The Virtual Registry classifies every request twice before it does anything else: by package type, which names the ecosystem the request belongs to, and by resource type, which says whether the request is for a manifest, a package, or something else.
The pair decides how long the response is cached, whether the request is sent through the Artifact Firewall, which URLs are rewritten in the response body, and which cache tags the object is stored under.
Classification needs no configuration. A registry does not declare which ecosystem it serves, so one registry can front an upstream that serves several.
Three signals are consulted in order, and the first that matches wins.
Accept header, for the vendor media types that name their own format: application/vnd.docker.* and application/vnd.oci.* for OCI, application/vnd.npm.* for npm, application/vnd.pypi.simple.v1+* for PyPI.User-Agent, which is what identifies most clients. The table below lists what is recognised.curl of a package URL or a client whose user agent is unknown.A request that matches none of the three is typed other and cached on the general rules, with no ecosystem-specific handling.
Requests reaching a repository manager rather than a registry carry a path prefix that the package path does not. Sonatype Nexus (/repository/<repo>) and JFrog Artifactory (/artifactory/<repo> and /<context>/api/<type>/<repo>) prefixes are recognised and stripped before the package coordinates are read, so the same rules and cache keys apply either way.
| Package type | Clients recognised by user agent |
|---|---|
oci |
docker, containerd, cri-o, containers |
npm |
npm, yarn, pnpm, bun |
pypi |
pip, twine, poetry, python-requests |
maven |
Apache-Maven, Gradle |
nuget |
NuGet, dotnet, VisualStudio |
hex |
Mix, hex_core |
cargo |
cargo |
conda |
conda, mamba, micromamba |
git, git-lfs |
git, git-lfs |
helm |
Helm |
php |
Composer |
deb |
Debian APT, apt-get, aptitude |
rpm |
libdnf, yum, rpm |
rubygems |
RubyGems, bundler |
conan |
conan |
ansible |
ansible, ansible-galaxy, ansible-runner, mazer, AWX |
apk |
apk |
bower |
bower |
chef |
Chef Client |
cocoa |
CocoaPods |
cran |
R |
dart |
Dart pub |
ivy |
Apache-Ivy |
opkg |
opkg |
puppet |
Puppet |
sbt |
sbt |
swift |
swift-package-manager |
terraform |
Terraform |
vagrant |
Vagrant |
The go and github types have no user agent of their own and are recognised from the URL, as are the ecosystems served under a repository manager path such as /api/npm/.
A repository manager serves a web UI alongside its package APIs, and that traffic is not package traffic. Three types cover it:
jfrog-ui, for JFrog’s /ui/ and /artifactory/api/oauth2/ paths.argocd-ui, for ArgoCD’s Dex login, auth callback and API paths.web, a last resort for a browser navigation that nothing else classified, meaning a Mozilla/5.0 user agent asking for text/html. ArgoCD’s client-routed pages sit at the bare root with no prefix to match on, which is what this catches. Scoping it to document navigations keeps a single-page app’s own JavaScript, CSS and fonts cacheable.All three bypass the cache, since every such response is per-session or per-user state, and all three opt out of transparent redirect following. A Dex or OIDC login binds a session cookie to its redirect, and following that redirect inside the Virtual Registry would strip the cookie before the browser saw it.
Some package types are parsed further, which is what lets the Virtual Registry tell a manifest from a package, extract the coordinates a firewall rule matches on, and rewrite the URLs a manifest hands back to the client:
oci, git, git-lfs, go, npm, pypi, maven, nuget, hex, helm, conda, cargo, conan, ansible, php, deb, rpm and github.
The rest are classified, counted and cached, but not parsed. Their requests are typed as other resources and cached on the general rules.
Go modules are served under /@v/ and /@latest, and the Virtual Registry also serves the Go checksum database at /sumdb/<name>/, forwarding to the database itself. A client that can reach nothing but the Virtual Registry can therefore still verify checksums, with GOPROXY as the only setting it needs. Only the two databases the go command ships with are proxied, sum.golang.org and sum.golang.google.cn. Any other name gets a 404.
A .pom at a released version is immutable and is cached for the package lifetime instead of being revalidated the way a manifest is. Resolving a dependency graph reads the POM of every candidate version, including ones it then discards, so revalidating each would cost a round trip per descriptor. A .pom under a -SNAPSHOT version is mutable and keeps revalidating.
Manifests are cached per tag and per digest, blobs by digest. A digest-addressed object is immutable and shared, and access to it is authorized per repository, so a client with access to one image is not served a layer belonging to an image it cannot reach.
The simple index is cached and filtered in both its HTML and its PEP 691 JSON form. The PEP 658 metadata sidecars (<file>.metadata) are cached too, which matters because a resolver fetches one per candidate wheel, more often than it downloads anything.
repomd.xml is the mutable entry point of a repository and revalidates on every request. Everything it points at is named by its own checksum, so an upstream change gives a new URL rather than new content at the old one, and those repodata files are cached for the immutable lifetime alongside the .rpm packages themselves.
Where the firewall is enabled, filtering the metadata invalidates the repository’s signature over it, which a client has to be configured for. See RPM enforcement.
Every request also lands in one of three resource types, each with its own default lifetime and its own TTL setting to override it:
manifest: a mutable reference, such as an npm packument, a PyPI simple index, a maven-metadata.xml, or a tag-addressed OCI manifest. Revalidated on every request by default, with requests for the same manifest coalesced into one fetch, and kept for a week so a stale copy can be served while the origin is unreachable. manifest_ttl replaces the revalidation with a fixed window.package: the artifact itself, which is immutable. Cached for a hundred years, capped by package_ttl.other: everything else, including every recognised-but-unparsed ecosystem and unclassified traffic. No lifetime of its own unless other_ttl sets one, so the origin’s cache headers decide, falling back to default_ttl.Some requests deviate from their resource type’s default because the protocol makes them immutable whatever their type: a digest-pinned OCI manifest, a Go .info or .mod, a PyPI metadata sidecar, a released Maven .pom, a checksum-named RPM repodata file. Those are cached for a hundred years and are unaffected by manifest_ttl.