Speed Up Maven in GitHub Actions
Last updated
To speed up Maven in GitHub Actions, cache the local repository in ~/.m2 through actions/setup-java, build modules in parallel with -T 1C, tune Surefire's forkCount, and stop running compile and test as separate full builds. A cold Maven build spends a surprising share of time downloading plugins and dependencies that never change.
This guide is the Maven version of our general guide to speeding up GitHub Actions.
Cache ~/.m2 with setup-java
actions/setup-java has a cache input. Its README says it accepts maven, gradle or sbt, and cache-dependency-path controls which files form the key (it supports globs and multiple lines). For Maven, the key comes from your pom.xml files.
- uses: actions/checkout@v7
- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: 21
cache: maven
- run: mvn -B -ntp verify
Two flags matter in CI. -B (batch mode) turns off interactive prompts and colors, and -ntp (--no-transfer-progress) removes the download progress noise that can make logs huge and slow. For a multi-module repo with poms in subdirectories, cache-dependency-path: '**/pom.xml' makes sure all of them feed the key.
A word on cache size. The ~/.m2/repository folder can grow with every dependency version you ever used. Entries expire after seven days without use, and the repository has a total limit, so see GitHub Actions cache limits. Do not cache target/; Maven's incremental detection is conservative and stale class files cause confusing failures.
Parallel builds with -T
Maven can build independent modules at the same time:
mvn -B -ntp -T 1C verify
-T 1C means one thread per CPU core. -T 4 means four threads. This only helps multi-module projects where modules do not all depend on each other in a chain. Check with mvn dependency:tree or the reactor summary. Be aware that some older plugins are not thread-safe; Maven prints a warning at startup for plugins that are not marked thread-safe. If you see random failures with -T, test with -T 1 to confirm the cause.
Surefire: forkCount and reuseForks
By default Surefire runs all test classes in one forked JVM. Two settings use more cores:
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<forkCount>1C</forkCount>
<reuseForks>true</reuseForks>
</configuration>
</plugin>
forkCountis how many JVMs Surefire starts at once.1Cmeans one per core, and a plain number such as2means two.reuseForks=true(the default) keeps each JVM for many test classes, which avoids JVM startup per class. Setting it tofalseisolates classes but is much slower.- JUnit 5 can also run tests inside one JVM in parallel with
junit.jupiter.execution.parallel.enabled=trueinjunit-platform.properties. That needs thread-safe tests.
Mind memory. Each fork has its own heap, and -T 1C multiplies it again. On a 7 GB hosted runner, -T 2 and forkCount=2 is safer than both at 1C. Watch for out-of-memory kills (exit code 137), covered in CI exit code 137.
Integration tests and Testcontainers
Failsafe (mvn verify) runs integration tests after packaging. If those use Testcontainers, container startup dominates. Reuse one container per test class hierarchy through a shared base class or the singleton container pattern instead of one per class. Docker image pulls also cost time on a fresh VM; the hosted runner has Docker, and images are pulled each run unless your runners keep a Docker cache. See Docker layer caching in CI.
Avoid doing the work twice
- One build, not three.
mvn verifyalready compiles, tests and packages. Runningmvn compile,mvn testandmvn packageas separate commands repeats work. - Skip what PRs do not need.
-Dmaven.javadoc.skip=trueand-DskipTestsin jobs that only need a jar. Use-Dcheckstyle.skiponly if lint runs elsewhere. - Offline after warm-up.
mvn -ofails when something is missing, which makes cache gaps visible. Use it only once the cache is reliable. - Maven daemon.
mvndkeeps a warm JVM, useful locally. On a fresh CI VM the daemon starts cold, so it gains little. - Build only changed modules.
-pl :module -ambuilds one module and what it needs. For a large reactor,-amdbuilds dependents. Combine with a path filter to skip the rest.
A complete workflow
name: build
on:
pull_request:
push:
branches: [main]
concurrency:
group: build-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
verify:
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: 21
cache: maven
cache-dependency-path: "**/pom.xml"
- run: mvn -B -ntp -T 1C verify
- uses: actions/upload-artifact@v7
if: always()
with:
name: surefire-reports
path: "**/target/surefire-reports"
What more CPU cores buy you
Maven parallelism depends on your module graph, and Surefire forks depend on your test count. Our benchmarks include a Spring Boot (Maven) + Kafka + Postgres project using Testcontainers: 1m54s on a GitHub-hosted runner and 1m12s on compiler.dev (1.6x), a full CI run measured on 8 October 2026. The gain is smaller than the Gradle stack in the same set (3.0x), which fits a build where Testcontainers startup and a short module chain limit what extra cores can do. See the Gradle guide for comparison.
To test your own build, change one line to runs-on: [self-hosted, Linux, compiler-ci]. Prices are on the pricing page, and there is a Maven template and a Java caching walkthrough.
FAQ
How do I cache Maven dependencies in GitHub Actions?
Use actions/setup-java with cache: maven. It caches ~/.m2/repository using a hash of your pom.xml files.
What does mvn -T 1C do?
It builds independent modules in parallel with one thread per CPU core. It helps multi-module projects and does nothing for a single module.
Is Maven's parallel build safe?
For most modern plugins, yes. Maven warns about plugins that are not thread-safe. Test with -T 1 if you see random failures.
Should I use mvnd in CI?
Usually not. The daemon's benefit is a warm JVM across builds, and a fresh CI VM starts cold.
Related
- Add a CI Speed Badge to Your READMEShow how fast your CI is with a README badge: the median GitHub Actions run time for your repository, updated automatically. Free for public repos.
- Speed Up .NET in GitHub ActionsSpeed up .NET builds in GitHub Actions: NuGet caching with setup-dotnet, --no-restore, --no-build, parallel dotnet test, and SQL Server services.
- Speed Up Bazel in GitHub ActionsSpeed up Bazel in GitHub Actions with setup-bazel, a disk cache, repository cache, a remote cache, and the right --jobs setting. Working YAML.
- Speed Up Django in GitHub ActionsSpeed up Django tests in GitHub Actions with pip or uv caching, pytest-xdist, --reuse-db, a Postgres service, and test splitting. Working YAML.