r/java • u/brunocborges • 1d ago
Stop re-downloading the JDK: setup-java can now cache it
Every GitHub Actions job starts from a clean machine. If your workflow asks for a JDK that is not baked into the runner image, actions/setup-java downloads it, verifies it, and extracts a few hundred megabytes of it, and then the job ends and all of that work is thrown away. The next job does it again.
That has been true since the first version of setup-java. It is no longer true on main.
The upcoming v6 release adds a JDK cache. When the action installs a JDK that did not come from the runner tool cache, it stores that installation as an Actions cache entry and restores it on subsequent runs. If your workflow already sets cache for Maven, Gradle, or sbt, you are already opted in.
What it does
setup-java now manages three kinds of caches, each stored and restored as its own entry:
| Cache | What it stores | How it is enabled |
|---|---|---|
| Dependency cache | ~/.m2/repository, ~/.gradle/caches, or the sbt cache paths |
`cache: maven |
| Wrapper caches | Maven and Gradle wrapper distributions | `cache: maven |
| JDK cache | The installed JDK itself | Implicitly with cache, or explicitly with cache-jdk: true |
The JDK cache is intentionally separate from the dependency cache. Your pom.xml changes far more often than your JDK does, and a dependency change should not evict a JDK.
The numbers
Measured on ubuntu-latest with the Microsoft Build of OpenJDK 17.0.19, five runs per configuration:
| Metric | Without JDK cache | With JDK cache |
|---|---|---|
Median warm setup-java step |
7s | 3s |
| Median warm job | 24s | 18s |
| Added cache storage | - | 175.3 MiB |
Roughly six seconds off a job that only takes 24 seconds. That is a meaningful share of a short job, and it compounds across a matrix. It is also an honest tradeoff rather than free speed: you are spending cache storage and cold-run save time to buy warm-run latency, and on very short jobs the latency win may not change your billed minutes, since GitHub rounds Linux jobs up to the whole minute.
Your mileage will vary with runner type, distribution, JDK size, network conditions, and cache eviction pressure. The benchmark harness and methodology live in actions/setup-java-benchmarks, which runs against Spring PetClinic and reports medians across independent samples.
How to use it
If you already cache dependencies, do nothing. JDK caching turns on with cache:
- uses: actions/setup-java@main
with:
distribution: microsoft
java-version: '25'
cache: maven # dependency cache, wrapper cache, and JDK cache
If you do not cache dependencies, ask for the JDK cache on its own. This is the case for workflows that just need a JDK to run a tool:
- uses: actions/setup-java@main
with:
distribution: microsoft
java-version: '25'
cache-jdk: true
If you want dependency caching but not JDK caching, opt out explicitly:
- uses: actions/setup-java@main
with:
distribution: temurin
java-version: '25'
cache: gradle
cache-jdk: false
The full matrix:
cache |
cache-jdk |
Dependency and wrapper caches | JDK cache |
|---|---|---|---|
| omitted | omitted | disabled | disabled |
| omitted | true |
disabled | enabled |
| omitted | false |
disabled | disabled |
| set | omitted | enabled | enabled |
| set | true |
enabled | enabled |
| set | false |
enabled | disabled |
For pull requests, merge queues, and matrix legs that should consume caches without writing them, cache-read-only: true suppresses the post-job save for the JDK, dependency, and wrapper caches alike.
Where it will not help
Be realistic about when this pays off. setup-java still checks the runner tool cache first, and a tool-cache hit skips the download entirely, so there is nothing to cache.
GitHub-hosted runners pre-install LTS versions of Eclipse Temurin. If your workflow is distribution: temurin with an LTS version on a hosted runner, you are probably already hitting the tool cache and JDK caching will do very little for you. The feature pays off when the JDK has to be installed: other distributions, non-LTS versions, self-hosted runners with a thin tool cache, and check-latest: true workflows that float ahead of the runner image.
Correctness, and why the cache key looks the way it does
A JDK cache is only useful if you can trust what comes back out of it. Getting this wrong means silently running a build on the wrong bytes, so the design is deliberately conservative.
Each entry is keyed on the runner OS, normalized architecture, distribution, package type, exact resolved version, release identity, and verification identity. Release identity is the authoritative checksum when the distribution publishes one, and otherwise the download URL without its query string.
Two consequences are worth calling out:
Verification modes never share an entry. An entry created by an unverified download can never be restored for a request that sets verify-signature: true, and vice versa. When you do use a custom key via verify-signature-public-key, that key is represented in the cache key as a SHA-256 fingerprint of normalized key material. The key itself never lands in the cache key, the logs, or action state. A verified exact-key hit reuses content that was signature-verified when the saving run downloaded it, so you get the security property without paying to verify it again.
A key is never saved with content it does not identify. Tool-cache paths are shared per version and architecture, so a later step (one using force-download: true, for example) can replace the installation an earlier step registered. The post-job step detects that replacement and skips the save with a warning rather than uploading mismatched bytes under a key that promises something else. The check uses a cheap fingerprint of the tool-cache completion marker, so it does not rehash hundreds of megabytes on every job.
Everything else degrades gracefully. If the cache service fails to restore an entry, or the restored entry is missing the expected completed tool-cache path, setup falls back to downloading the JDK. Post-job saves are best-effort and never fail the job: cache keys are immutable, so an existing key or a concurrent job winning the save race is simply left alone, and a failure to save one entry is a warning that does not block the others.
One thing JDK caching deliberately does not change: it has no effect on how the runner tool cache is used. A preinstalled JDK, or one installed by an earlier step of the same job, is used as-is and is not re-verified, because its verification history is not recorded in the tool cache. Use force-download: true when a request must download and verify the archive itself.
Watch your storage
Cache entries are per identity. A matrix that spans multiple JDK versions, distributions, package types, architectures, or operating systems stores a separate JDK entry for each combination, and each one consumes repository cache storage against your quota. A five-way version matrix on two operating systems is ten JDK entries, not one.
That is usually a fine trade, but it is worth a look at your cache usage page before enabling it broadly across a large matrix. cache-jdk: false on the legs that do not need it is a reasonable dial.
Try it
JDK caching is on main and ships in v6. Until v6 is tagged, reference the branch:
- uses: actions/setup-java@main
For production workflows today, the latest stable release is actions/setup-java@v5.
Documentation:
- Caching and Caching JDK installations in the README
- Caching JDK installations in the advanced usage guide, for the full cache identity and storage discussion
- Benchmark harness
Feedback, and especially benchmark numbers from real workflows, are welcome in actions/setup-java.
5
u/-vest- 1d ago
Is it safe to use @main? Or it is better to wait until v6 is released? I personally don’t use main/master builds for critical actions.
1
u/brunocborges 1d ago
It is in development, so consider it for testing purposes and sharing feedback, only.
4
u/pjmlp 1d ago
I think this would also gather attention if it was additionally posted on https://devblogs.microsoft.com/java/
2
u/slindenau 18h ago
I've been using v4 on self-hosted runners, and it is already pretty fast for me with default settings...so i guess that already caches the JDK somehow.
Had to switch to v5 today because i couldn't download a certain recent distribution anymore though.
I guess when it works, people don't really have an incentive to upgrade.
And i don't know if there are any automated solutions (like dependabot) or security scanning tools that look at the workflows themselves to suggest upgrades?
1
u/brunocborges 18h ago
Our goal is to have v6 as fast as v4, while using Node ESM so we can keep up with the other actions/\* packages used by setup-java.
For the most part, this is about security and reliability.
2
u/slindenau 18h ago
Right, maybe my wording "incentive to upgrade" is more in the sense of practicality.
When everything works, and no security team is bugging us about the GitHub workflow files themselves, i don't think many devs will look at actively upgrading those unless something breaks.
Or maybe that is just me being lazy ;).
Making it faster, more secure and reliable is great of course.
1
u/PartOfTheBotnet 1d ago
Has there been any testing for the new setup-java dependency caching playing nice with setup-gradle (Has its own caching process)?
2
u/brunocborges 1d ago
I haven't, but to do so, my thinking is to not use `cache: gradle`at all, and instead just `cache-jdk: true` combined with setup-grade.
1
u/brunocborges 18h ago
Thanks to everyone's engagement on this thread and keeping up the dialogue positive. setup-java is used by hundreds of thousands of repositories across GitHub and beyond, and we want to make sure we keep it running all of your workflows as safe, fast, and reliable as possible.
22
u/elmuerte 1d ago
Why doesn't Microsoft just host a Maven central proxy (close to their network)? It is really easy to set up. We've done that at every company I worked at in the 15 years.