ARTIFACTORY: 429 Rate Limits from Maven Central / Public Upstreams Surface as 404s
Builds resolving dependencies through Artifactory may return intermittent 404s for artifacts that demonstrably exist on the upstream — when the real cause is a 429 (rate limit) response from the public upstream, cached briefly by Artifactory as a "not found".
This pattern has become more common since Sonatype began enforcing consumption limits on Maven Central in 2025. Thresholds are tightening over time, and enforcement now extends to organization-level limits — traffic spread across multiple IPs in the same organization is aggregated against a single quota. Other public registries (Gradle Plugins, npm, jOOQ, Spring) apply similar throttling.
|
GAV — Maven shorthand for how an artifact is uniquely identified: |
Symptoms
-
404 from a virtual repository for artifacts known to exist on the upstream (e.g. a recent org.springframework.boot release) -
Fast response time on the 404 — usually under 100 ms, often 0–30 ms -
artifactory-request-out.log shows 429s on the public upstream in the same window, or shows no outbound entries at all for the failing request (cached miss) -
Build tools surface this as a "not found" error, e.g.:-
Maven: Could not transfer artifact ... not found -
Gradle: Could not resolve ... Received status code 404
-
-
The issue sometimes "resolves itself" 30+ minutes later with no config change
Why this happens
When a public upstream returns 429, Artifactory records the path in its missed retrieval cache for missedRetrievalCachePeriodSecs (default 1800 s / 30 min). During that window, Artifactory serves the path from the negative cache as "not found" — without contacting the upstream again. This is deliberate protection against thundering-herd retries against an upstream that's already throttling.
If no other member of the virtual repository can serve the artifact, the virtual returns 404 to the client. The 429 never reaches the client.
One nuance worth knowing: maven-metadata.xml requests aggregate across every virtual member regardless of which locals contain the artifact. Metadata calls touch public upstreams by design and are a partial contributor to rate-limit pressure.
What to do
Listed in order of leverage — duplicates and excludesPattern are typically the highest-impact changes.
1. Consolidate duplicate remotes
Multiple remotes pointing to the same upstream URL (or to equivalent mirrors, e.g. repo1.maven.org/maven2 and repo.maven.apache.org/maven2) each maintain their own negative cache slot. A single 429 event seeds the cache for every duplicate, and a virtual lookup may issue the same upstream call multiple times.
Inventory remotes by upstream URL:
curl -s -u admin:<token> "$JFROG_HOST/artifactory/api/repositories?type=remote" | jq -r '.[].key' | \ while read k; do url=$(curl -s -u admin:<token> "$JFROG_HOST/artifactory/api/repositories/$k" | jq -r .url) echo "$url|$k" done | sort
Keep one remote per upstream — ideally one with storeArtifactsLocally=true, retrievalCachePeriodSecs=7200, and bypassHeadRequests=true. If feasible, remove the others from any virtuals that include them.
2. Set excludesPattern for internal organization paths on public remotes
Setting excludesPattern: com/<your-org>/** (and equivalents — org/<your-org>/**, etc.) on every public remote is the universal fix. It applies before any network call — before metadata aggregation, before virtual fall-through, regardless of how your virtuals are arranged.
This matters most when public remotes and internal locals share a virtual, because maven-metadata.xml aggregation always walks every member. If you instead expose public remotes as their own separate virtual and your builds query both, you have a second option: scope internal groupIds to your locals-virtual at the build layer. Both work — excludesPattern is the catch-all that doesn't require touching every build config.
Configure on the remote repo under Advanced > Excludes Pattern, or via REST:
{ "excludesPattern": "com/<your-org>/**,org/<your-org>/**" }
3. Enable bypassHeadRequests
Artifactory's default fetch flow is HEAD-then-GET — two upstream calls for every fresh fetch. With bypassHeadRequests=true, Artifactory skips the HEAD probe and goes straight to GET, reducing outbound request volume.
The value here is request-volume reduction — GETs are not rate-limited less aggressively than HEADs. Actual savings depend on cache hit rate: a fully-cached repo sees little benefit, a frequently-fresh repo sees close to 50%.
Applies primarily to Maven, Gradle (Maven layout), and Ivy remotes. Some package types handle the HEAD/GET flow differently; check before enabling on non-Maven-family remotes.
4. Verify storeArtifactsLocally=true and a sane retrievalCachePeriodSecs
-
storeArtifactsLocally=false means every successful fetch re-hits upstream, with no local copy to serve subsequent requests from. Almost always should be true -
retrievalCachePeriodSecs defaults to 7200 (2 h). Dropping it to 600 to "see updates faster" comes at a real cost: every artifact gets re-validated against upstream every 10 minutes. For Maven Central releases (which are immutable), 7200 or higher is appropriate
Worth knowing
If outbound logs show specific-version GAV paths like com/<your-org>/<artifact>/<version>/<artifact>-<version>.jar going to public upstreams, the virtual is falling through the locals because none of them returned the artifact. That's a separate investigation — which local was supposed to serve those artifacts, and why isn't it?
Related