Jackson Mafra
03/25/2026, 1:23 PMtrevjones
03/25/2026, 4:11 PMWhat about enterprises running self-hosted runners at scale?if you are self hosting you should absolutely rebuild every cache action you use that are backed by your own S3 storage instead of using the actions/cache* stuff. You stand to save a ton of time by keeping the heavy file moves local to the cloud region you are hosting in. Assuming you have an S3 vpc endpoint configured it will also be eliminating a TON of NAT gateway bandwidth saving you thousands per month before even considering the time savings for your engineering team. (Aws phrasing in all of that but pretty sure you could replicate everything in any of the major clouds)
trevjones
03/26/2026, 12:38 AMCLOVIS
03/26/2026, 3:30 PMBy submitting User Submissions through the Services, you hereby do and shall grant Gradle a worldwide, non-exclusive, perpetual, royalty-free, fully paid, sublicensable and transferable license to use, edit, modify, truncate, aggregate, reproduce, distribute, prepare derivative works of, display, perform, and otherwise fully exploit the User Submissions in connection with this site, the Services and our (and our successors’ and assigns’) businessesSo everything that ends up in the build cache belongs to them now
sindrenm
03/26/2026, 11:57 PMFor clarity, the foregoing license grants to us and our users do not affect your other ownership or license rights in your User Submissions, including the right to grant additional licenses to your User Submissions, unless otherwise agreed in writing.
CLOVIS
03/27/2026, 9:02 AMsindrenm
03/27/2026, 9:34 AMtrevjones
03/31/2026, 5:11 AMHere's the full picture:
---
tool-cache-s3: Backs the runner's tool cache directory to/from S3. On restore, it downloads a manifest (manifests/{cache-key}.json) and fetches
tar.zst archives in parallel into _tool/{name}/{version}/{arch}/. On save (post step), it scans for new .complete markers, tars them up, and
uploads. Same directory layout as GitHub's @actions/tool-cache.
setup-android: Starts a local HTTPS proxy that intercepts sdkmanager downloads. On cache hit, it serves files from the tool cache directory. On
cache miss, it downloads and writes them into the tool cache with .complete markers — which tool-cache-s3's post step then picks up and persists
to S3. Next run, tool-cache-s3 restores them and the proxy serves them locally. Net effect: Android SDK packages download once, then get served
from S3 cache on every subsequent run.
Parallelization — two levels:
1. Across tools: Both restore and save dispatch all tools concurrently via Promise.all(manifest.tools.map(...)). Every tool in the manifest
downloads/uploads at the same time rather than sequentially.
2. Within each S3 object (the s3.ts layer): Large files (>32MB) use parallel byte-range transfers:
- Download: HEAD to get size → split into 32MB ranges → fire up to 10 concurrent Range GET requests → write each chunk at its offset into a
pre-allocated file. Uses a bounded-concurrency pattern (Set<Promise> + Promise.race when pool is full).
- Upload: AWS SDK Upload with 32MB parts and queueSize: 10, which streams 10 multipart upload parts in flight simultaneously.
- Small files (≤32MB): skip the range machinery and do a single GET/PUT — avoids the HEAD + range overhead.
So a restore of 8 tools fires 8 parallel downloads, each of which internally parallelizes into 10 concurrent 32MB range GETs. Same pattern on
save. The result is saturating the EC2-to-S3 network link rather than bottlenecking on sequential transfers.trevjones
03/31/2026, 5:11 AMThe _tool directory (usually /opt/hostedtoolcache or $RUNNER_TOOL_CACHE) is a convention shared between GitHub's @actions/tool-cache npm package
and all the official actions/setup-* actions.
When actions/setup-node (or setup-java, setup-python, etc.) runs, it calls tc.find('node', '20.x', 'x64') which checks
_tool/node/20.11.1/x64/.complete. If that marker file exists, it skips downloading entirely and just adds the directory to PATH. If not, it
downloads, extracts, calls tc.cacheDir() which writes the files and creates the .complete marker.
tool-cache-s3 exploits this directly. It restores tar.zst archives into the exact same _tool/{name}/{version}/{arch}/ layout with .complete
markers before any setup-* action runs. When actions/setup-node later calls tc.find(), it sees the marker, treats it as a cache hit, and skips
the download. The setup action has no idea S3 was involved — it just sees a pre-populated tool cache directory.
On the save side, any tool installed during the job (cache miss on restore) gets a .complete marker written by the setup action's tc.cacheDir()
call. tool-cache-s3's post step scans for these markers, tars up anything new, and uploads to S3 for next time.
So it's a transparent layer that sits beneath the standard tool-cache API — no changes needed to any actions/setup-* action.trevjones
03/31/2026, 5:14 AM