How we made GitHub Actions cache up to 6x faster

leolannenmaki1 pts0 comments

How we made GitHub Actions cache up to 6× faster | Avrea Blog

Skip to Main Content

Go to home page

Pricing<br>Resources

Company

Try for Free<br>Try for Free

Sign In<br>Sign In

Try for Free<br>Try for Free

Sign in<br>Sign in

Back to all blogs<br>Back to all blogs

CI/CD

How we made GitHub Actions cache up to 6× faster<br>On GitHub Actions, a 1 GB actions/cache restore takes 10-20 seconds. On Avrea, the same workflow, same client: less than 3 seconds. Up to 6× faster across cache sizes from 1 KB to 1 GB, with 3× tighter variance.

Leo Lännenmäki<br>21 May 2026

During my first months at Avrea, engineering cache infrastructure has been by far my favorite challenge to work on. The work started with actions/cache, which powers much of the actions ecosystem. All of the setup-* actions call it under the hood: setup-node, setup-python, setup-go, setup-java, setup-dotnet. So does Docker Buildx's type=gha backend.<br>Making actions/cache fast makes the rest of the ecosystem fast. And on GitHub-hosted runners, it's slow.<br>Why the default is slow<br>The interface of actions/cache is good. The amount and versatility of tools built on top of it are a testament to it. We were only looking to make it faster.<br>GitHub stores cache entries in Azure Blob Storage. Your GitHub workflows most likely also run on Azure, but the latency between these two is not great.<br>The protocol used adds to the latency. The v2 GHA cache API uses Twirp, and a download is two phases: first you call GetCacheEntryDownloadURL, get back a signed URL, then you do the actual GET. That’s an extra round-trip before bytes flow. Now you have a floor on cache speed that’s “RTT to wherever the cache lives, twice, plus the time the transfer takes.”

Sequence diagram of the actions/cache restore flowWe can't collapse the two phases as the logic is baked into the client. But putting the API layer on the same host and co-locating the storage layer mitigates the problem. The harder thing is getting the runner to actually use our cache instead of Azure.<br>Why making the cache fast is hard<br>It’s relatively easy to build a fast cache in isolation. But that’s not the problem we are solving here. The actual problem is closer to “build a drop-in replacement for an existing cache, with all of its constraints, that also happens to be fast.”<br>There are six things you don’t get to change:<br>(Most of) the environment. GitHub Actions Runner Images execute GitHub workflows. What’s installed on the machine and how you interact with it is fixed. We could ask the user to make changes to their workflow yaml, but we’d rather change the runtime than the workflow. Luckily we can choose the machines that run the runners. And we chose fast machines.<br>The client. actions/cache is JavaScript running inside Node inside the user’s runner. That’s code outside of our control and that would be hard to change without breaking backwards compatibility or requiring the user to adopt another action. So every optimization has to live outside of the environment and the client code.<br>The protocol. The client speaks a specific set of Twirp calls and uses a specific subset of Azure Blob HTTP semantics.<br>The security model. GitHub Actions cache has access rules: branch-scoped reads, default-branch fallback, base-ref access for PRs. It’s difficult to secure GitHub Actions in general. At the bare minimum we must enforce the same scoping rules and security boundaries GitHub does itself.<br>The VMs are ephemeral. Every job runs in a fresh VM that is killed when the job ends. So nothing, including caches, can persist on the machine itself. Some form of attached storage, like sticky disks, is one solution but that comes with other tradeoffs. GitHub’s choice, and ours, is a network call to a cache service.<br>Scale. The storage layer must sustain high concurrent request volume and hold cache entries for all of our customers.<br>Making the cache go fast means making the cache go fast within these constraints.<br>A local proxy that speaks the cache protocol

Architecture overviewOur hosts ship with a small Go program that listens on :443 and speaks the same TLS endpoint the GitHub cache client expects to find. The runner image trusts a CA we provision and the proxy presents a cert signed by that CA.<br>The proxy splits traffic two ways. Cache paths (/twirp/github.actions.results.api.v1.CacheService/* and /blobs/*) it handles locally. Everything else it forwards untouched to GitHub.<br>The locally-handled side implements a GitHub-compatible API surface. It’s only four endpoints (CreateCacheEntry, FinalizeCacheEntryUpload, GetCacheEntryDownloadURL, plus PUT/GET /blobs/...). The blob endpoints speak Azure’s block blob upload semantics for the subset the actions/cache client requires.<br>Every request carries the workflow’s ACTIONS_RUNTIME_TOKEN, a JWT signed by GitHub. We verify the signature against GitHub’s JWKS and use two claims to scope the request: repository_id and ac, a JSON-encoded list of {ref, permission} entries. The ac claim controls which refs the...

cache github actions fast client setup

Related Articles