# maven could not transfer artifact ci, not a missing jar

> A maven could not transfer artifact ci failure is a transport error, not a missing jar. Tell the two apart, then set the retry property that is read.

Source: https://latchkey.dev/learn/failures/maven-could-not-transfer-artifact-in-ci  
Updated: 2026-09-20

A maven could not transfer artifact ci failure means the resolver tried to move bytes and could not, which is a different failure from the repository telling Maven the artifact is not there. Maven prints two distinct messages for those two situations and caches exactly one of them by default, so most of the advice written about cached failures and retry properties is attached to the message you do not have.

## What this error means

The build stops while Maven is resolving something, and the header tells you what. A dependency failure opens `[ERROR] Failed to execute goal on project ...: Could not resolve dependencies for project ...`; a plugin failure opens `[ERROR] Plugin ... or one of its dependencies could not be resolved`. Under either header the lines that matter are the same, naming an artifact and a repository, and the words in them matter more than anything else in the log. "Could not transfer" is a transport failure. "Could not find" is a 404. The two come from different exception classes in the resolver and they need opposite fixes. The block below is the plugin shape, header and all, produced on purpose in a container running Apache Maven 3.9.16 on Temurin 21.0.12, with a `<mirrorOf>*</mirrorOf>` entry pointing every repository at a local server that answers 503 to everything. The `Could not transfer artifact` clause reads the same under a dependency header. This page carries no reproduction on a Latchkey runner, so the repository id and URL are ours, and the shape either side of them is what to compare against your log.

```Apache Maven 3.9.16, mirror answering 503, plugin resolution
[ERROR] Plugin org.apache.maven.plugins:maven-clean-plugin:3.2.0 or one of its dependencies could not be resolved:
[ERROR] 	The following artifacts could not be resolved: org.apache.maven.plugins:maven-clean-plugin:pom:3.2.0 (absent): Could not transfer artifact org.apache.maven.plugins:maven-clean-plugin:pom:3.2.0 from/to house-mirror (http://mirror503:8899/maven2): status code: 503, reason phrase: Service Unavailable (503)
```

## Common causes

### The repository, mirror or proxy in front of it refused or stalled

A 5xx from a Nexus or Artifactory instance under load, a connection that never completes because a firewall is dropping rather than rejecting, or a corporate proxy that closes mid transfer. The cause clause after the colon names which: `Connect timed out` is a connection that never happened, `status code: 503` is a server that chose that answer.

### A transfer stalled, and the default gives it thirty minutes to do so

A firewall that drops rather than rejects, or a proxy that accepts a connection and then stops forwarding, leaves the resolver waiting on `aether.connector.requestTimeout`, which defaults to 1800000 milliseconds. Most CI jobs have a shorter timeout than that, so the job is killed before Maven ever prints an error, and the log just stops in the middle of dependency resolution. If you are reading a truncated log rather than a `Could not transfer` line, this is usually why.

### Credentials are missing, so the repository answered with a challenge

A private repository that expects authentication will answer 401 or 403, and Maven reports that as a transfer failure with the status in the cause clause. It reads like an outage and is not one. The `<server>` entry in `settings.xml` has to match the `<id>` of the repository or mirror exactly, and CI is where that match is usually wrong because the file is generated.

### Snapshot metadata is being re-resolved on every build

Snapshots are looked up through `maven-metadata.xml` every time the update interval expires, so a snapshot heavy build makes many more requests than a release only build and is correspondingly more exposed to a bad minute. In our experience most "Maven is flaky in CI" reports from a team come from one module that depends on a snapshot it does not need to.

## How to fix it

### Read the cause clause, then choose the lane

1. If it says `Could not find artifact`, stop looking at the network. Check the version, whether the upstream module deployed, and whether the repository that holds it is declared. The last fix below is that lane's repair.
2. If it says `Could not transfer artifact` with a status code, the repository answered. Treat it as a server problem and retry.
3. If it says `Could not transfer artifact` with a connect or read timeout, the repository did not answer. Treat it as egress and look at proxies, DNS and firewalls.

```Terminal
mvn -B -ntp -e dependency:resolve 2>&1 | grep -E "Could not (transfer|find)"
```

### Set the retry count and the timeout on the transport Maven is using

Three retries is already the native default, so the setting worth changing is the timeout: a minute instead of thirty stops a stalled transfer from holding the job until the job timeout kills it. Put both in `MAVEN_OPTS` so every module in a reactor build inherits them, and use the `aether.connector.` names. The `maven.wagon.` equivalents will be accepted on the command line and quietly ignored.

```.github/workflows/ci.yml
env:
  MAVEN_OPTS: >-
    -Daether.connector.http.retryHandler.count=3
    -Daether.connector.requestTimeout=60000
```

### Point at one mirror you control, and authenticate it properly

A single mirror with `<mirrorOf>*</mirrorOf>` makes the failure mode legible: one host, one set of credentials, one place to look. Generate `settings.xml` in the workflow so the `<server>` id matches the mirror id, and keep the token in a secret rather than in the file you committed.

```settings.xml
<settings>
  <mirrors>
    <mirror>
      <id>house</id>
      <url>https://nexus.example.com/repository/maven-public/</url>
      <mirrorOf>*</mirrorOf>
    </mirror>
  </mirrors>
  <servers>
    <server>
      <id>house</id>
      <username>${env.NEXUS_USER}</username>
      <password>${env.NEXUS_TOKEN}</password>
    </server>
  </servers>
</settings>
```

### If your log says the other thing, clear the cached not found

This repair is last because on this page it is usually not yours. It belongs to the message that says `was not found ... during a previous attempt`, where the answer is on disk rather than on the network: force the check with `-U` or delete the marker files before the build. Reach for it when a dependency you just published still reports as missing, and not when the message says `Could not transfer`, because that result was never cached in the first place.

```.github/workflows/ci.yml
- uses: actions/cache@v4
  with:
    path: ~/.m2/repository
    key: m2-${{ runner.os }}-${{ hashFiles('**/pom.xml') }}
- run: find ~/.m2/repository -name "*.lastUpdated" -delete
- run: mvn -B -ntp -U verify
```

## How to prevent it

- Delete `*.lastUpdated` files before the build when you restore `~/.m2` and expect a newly published artifact to be visible.
- Set `aether.connector.requestTimeout` to about a minute instead of leaving the thirty minute default in place.
- Put retry and timeout settings under the `aether.connector.` names, and delete any `maven.wagon.` ones a previous generation of advice left in the workflow.
- Keep the `<server>` id and the `<mirror>` id identical, and generate `settings.xml` in the workflow rather than committing it.

## Transfer or find: read the verb

These are not two phrasings of one condition. In Maven Resolver, `ArtifactTransferException` builds its message as "Could not transfer artifact " followed by the coordinates, the repository, and the underlying cause, while `ArtifactNotFoundException` builds "Could not find artifact " followed by the coordinates and the repository. One is raised when the transport failed. The other is raised when the transport worked and the answer was that there is no such file.

In the same container, pointing the build at the real Maven Central and asking for a version that does not exist produced the other message, with no transport problem anywhere in it. If your log says this, no amount of retrying, timeout tuning or mirror swapping will help, because nothing failed to move.

```Apache Maven 3.9.16, real Maven Central, version that does not exist
[ERROR] Failed to execute goal on project repro: Could not resolve dependencies for project com.example:repro:jar:1.0-SNAPSHOT
[ERROR] dependency: com.google.guava:guava:jar:33.4.0-nope (compile)
[ERROR] 	Could not find artifact com.google.guava:guava:jar:33.4.0-nope in central (https://repo.maven.apache.org/maven2)
```

| Message | What it proves | The fix that works |
| --- | --- | --- |
| Could not transfer artifact ... : Connect timed out | No connection to the repository host. | Egress, proxy settings, then a retry handler count. |
| Could not transfer artifact ... : status code: 503 | The repository answered and refused. | Retry. Maven does not cache this one, so the next run goes out again. |
| Could not find artifact ... | The repository answered that it has no such file. | A wrong version, a missing deploy, or a repository not declared. This one is cached. |
| Could not transfer metadata ... maven-metadata.xml | A snapshot or version listing failed to move. | The same transport fixes. `-U` forces the metadata update interval check, which is a separate mechanism from error caching. |

## Maven caches one of these two failures, and not the one you came here for

The story everybody repeats is that Maven writes a failed download into the local repository and refuses to retry it until an update interval passes, so one bad minute keeps failing builds all afternoon. That story is true. It is about the other message. The Maven command line switches failure caching on for not found and leaves it off for transport errors, in two adjacent lines of `MavenCli`: `request.setCacheNotFound(true)` and `request.setCacheTransferError(false)`. Those feed the resolution error policy the session is built with.

The behavior follows, and you can watch both halves. Running the same command a second time against the mirror that was still answering 503 produced the same live `Could not transfer artifact` line, and the request arrived at the server again. Running it a second time against real Maven Central for a version that does not exist produced no request at all, and this instead.

Adding `-U` to that third run brought the live `Could not find artifact` wording straight back, which is what "updates are forced" in the message means. So `-U` and deleting `*.lastUpdated` are real fixes with a real mechanism behind them. They are fixes for the message this page is not about. If your log says `Could not transfer`, nothing is being replayed from disk by default, and reaching for `-U` will not change what happens next.

```Apache Maven 3.9.16, the second run of a not-found resolution
[ERROR] 	com.google.guava:guava:jar:33.4.0-nope was not found in https://repo.maven.apache.org/maven2 during a previous attempt. This failure was cached in the local repository and resolution is not reattempted until the update interval of central has elapsed or updates are forced
```

> The record is written either way. After the 503 run, `maven-clean-plugin-3.2.0.pom.lastUpdated` held an `.error=` line carrying the whole transfer message. The file existing is not the same as resolution being skipped. The error policy decides that, and for a transport error it says go and ask again.

## The retry property everyone recommends does nothing here

Search for Maven retries and you will be told to set `maven.wagon.http.retryHandler.count`. On Maven 3.9.16 that property is inert, because wagon is not the transport doing the work. Maven leaves transport selection on automatic unless you override it, and selection goes by priority: in resolver 1.9.27 the native `HttpTransporterFactory` declares a priority of 5.0 and `WagonTransporterFactory` declares -1.0. The native transport wins, and it reads `aether.connector.` properties, not `maven.wagon.` ones.

Counting requests at the 503 mirror for a single artifact settles it without any need to trust the reasoning.

The native defaults live in the resolver's `ConfigurationProperties`: `aether.connector.http.retryHandler.count` defaults to 3, which is the four attempts in the first row, and `aether.connector.requestTimeout` defaults to 1800000 milliseconds, thirty minutes. That timeout is the one to move in CI. A stalled transfer will otherwise sit there for half an hour, which in practice means your job timeout fires first and the log ends with no Maven error in it at all.

The wagon documentation is not wrong, and it is worth reading if you deliberately select that transport: `maven.wagon.rto` is its read timeout, also defaulting to 1800000, and the retry handler has been configurable through system properties since wagon 3.2. It also warns that pointing `maven.wagon.http.retryHandler.class` at a class of your own "will not work with the shaded version bundled with Maven". None of it applies until you pass `-Dmaven.resolver.transport=wagon`, and the last row above is a good reason not to.

```Terminal
mvn -B -ntp \
  -Daether.connector.http.retryHandler.count=3 \
  -Daether.connector.requestTimeout=60000 \
  verify
```

| What was on the command line | Requests for one artifact | What that shows |
| --- | --- | --- |
| Nothing | 4 | The native default: one attempt plus three retries. |
| `-Dmaven.wagon.http.retryHandler.count=6` | 4 | Unchanged. The wagon property is never read. |
| `-Daether.connector.http.retryHandler.count=6` | 7 | One attempt plus six. This is the live knob. |
| `-Dmaven.resolver.transport=wagon` and the wagon count | 1 | Wagon retries transport exceptions, not a 503. |

## Why this page has no runner reproduction

The failure needs a repository that misbehaves on demand, and the honest way to get one is to run a server that misbehaves. That is a container, not a runner job. Billing runner minutes to reach the same 503 through a longer path would produce the same string with a Latchkey hostname in it, and a hostname is not evidence.

The container also bought the two things a runner log could not have shown. Counting requests at the server is what proved which retry property Maven reads, and running the same command twice against the same server is what separated the failure Maven caches from the one it does not. Both of those are claims this page makes, and neither survives being taken on trust.

The slug is absent from `content/heal-evidence.mjs`, so this page marks nothing `healable` and describes no repair. What Latchkey does with a failing Maven step in general is covered in [how self-healing works](/documentation/self-healing); what it does with this specific failure is not something this page is in a position to state.

## FAQ

### What is the difference between Could not transfer artifact and Could not find artifact?

They come from two different exceptions in Maven Resolver. "Could not transfer" means the bytes did not move: a timeout, a reset, a 5xx or an auth challenge. "Could not find" means the repository answered and said it does not have that artifact. Retrying helps the first and never helps the second.

### Why does Maven keep failing after the repository comes back?

If the message is `Could not find artifact`, because that result was cached: Maven runs with not-found caching on, and the next run says so with a line about a previous attempt. Force the check with `-U` or delete the `*.lastUpdated` markers. If the message is `Could not transfer artifact`, caching is not the reason, because the Maven command line leaves transfer-error caching off, and a second run does go back to the network.

### How many times does Maven retry a failed artifact download?

Four attempts in total on Maven 3.9.16, which we counted against a mirror answering 503: one try plus the three that `aether.connector.http.retryHandler.count` allows by default. Raise that property to change it. The widely recommended `maven.wagon.http.retryHandler.count` had no effect on the count in the same test, because the default transport is the resolver native one rather than wagon.

### Should I cache the Maven local repository in GitHub Actions?

Yes, it is usually the largest single saving in a Java pipeline, but cache the artifacts and not the bookkeeping. Delete the `*.lastUpdated` files after restoring. Those carry a cached not-found decision between runs, which is what makes a dependency you published an hour ago stay invisible until the update interval expires.

## References

- [Maven Resolver 1.9.27: ArtifactTransferException builds the "Could not transfer" message](https://github.com/apache/maven-resolver/blob/maven-resolver-1.9.27/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/ArtifactTransferException.java)
- [Maven Wagon HTTP: retryHandler and rto system properties](https://maven.apache.org/wagon/wagon-providers/wagon-http/)
- [Maven Resolver 1.9.27: ConfigurationProperties, the aether.connector defaults](https://github.com/apache/maven-resolver/blob/maven-resolver-1.9.27/maven-resolver-api/src/main/java/org/eclipse/aether/ConfigurationProperties.java)
- [Apache Maven 3.9.16: MavenCli sets cacheNotFound true and cacheTransferError false](https://github.com/apache/maven/blob/maven-3.9.16/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
