In July 2025, I was installing Ruby on an old MacBook Air with 16 GB of RAM. I was tinkering with a side project, and compiling Ruby was taking longer than I wanted. I started wondering whether I needed to compile it at all. GitHub Actions had macOS runners and prebuilt Ruby versions. Could I use one of those?
It was not as simple as downloading a build and running it on my laptop. The Ruby builds used by GitHub Actions embed their installation path and depend on the environment they were built for. A build existing somewhere did not mean I could use it where I needed it.
The first experiment was precompiled Ruby
My first idea was to compile Ruby, store the builds somewhere, and let other people download them. If teammates were using the same version on compatible machines, why should each person wait for the same compilation?
Looking back, it was a small problem to start a product around. I did not install Ruby every day. But the wait bothered me enough to try, and looking through the existing builds made me think about the storage and computation involved. Many machines were keeping copies of the same software and doing similar work. I wanted to know how much of that work we could share.
During those experiments, I came across Dagger and used it to build my own pipeline. I could define the individual steps and work with their cached results. The earliest BoringCache alpha cached the work done by pipeline steps so it could be reused on a later run. Installing dependencies, compiling code and building containers all produce results that can be useful again. I became more interested in making those results available to the next build.
Caches depended on where the build ran
The caches I used were usually part of something else: a CI service, a deployment platform or a particular machine. They served a useful purpose there. The difficulty came when I wanted to use the same work somewhere else. A cache available to CI was not necessarily available to my laptop, and moving to another provider could mean building it again.
I wanted to choose where to run a build without also deciding where all its reusable work had to stay. A teammate's machine, a CI runner and a deployment host might all need the same dependency or compiled output. Where that output came from mattered less than whether it was compatible with the build that needed it.
Larger machines helped, but I was still waiting
I also tried running Docker builds on remote machines, including much larger runners. They were faster, but the improvement in my experiments was smaller than I had hoped. Builds that took several minutes locally still took minutes remotely. Adding cores did not make every part of those builds finish proportionally faster. That experience changed what I wanted to optimize. Before paying for more compute, I wanted to reduce how much work the machine had to do. A faster machine can shorten a compilation. Reusing a compatible result can avoid that compilation altogether.
Other people building CI services were making the case for caching too. Blacksmith's Cache is King explains how Docker layer caching lets subsequent builds reuse earlier work. Keeping cache on fast storage near a runner makes sense. I wanted to make shared cache useful independently of the company supplying the runner.
I wanted to do more with less: make better use of existing machines, avoid repeated computation and store fewer copies of the same data. Being careful with those resources was a main premise of BoringCache, and it still guides what I work on. I sometimes wonder whether I am being too ambitious in trying to match services that keep fast storage close to their runners. But I think careful engineering can make up much of that performance difference.
I wanted caching to be the main job
That became the reason to build BoringCache. I wanted to focus on storing, sharing and reusing build results independently of a runner service. The cache should remain useful when the machine changes, whether the next build runs locally, on AWS, on Google Cloud or in another CI system.
I was also heavily influenced by the save-and-restore model in GitHub Actions. I had worked with teams that saved files to S3 and restored them with custom scripts. I wanted to offer the same save-and-restore workflow without each team having to write and maintain those scripts. Those experiences influenced the design of BoringCache's save and restore commands. I wanted saving a file or directory, then restoring it wherever it was needed, to be simple and fast. How those operations felt to use mattered to me too. I cared about the design of that interaction: clear names, few steps, and an interface that was easy to understand.
Cost was part of that idea from the beginning. Reducing repeated computation would be less useful if storing and retrieving its results became too expensive. Object storage appealed to me because it was widely available and let me separate the cost of keeping build results from the cost of running builds. I wanted affordable shared storage without requiring people to buy their compute from me.
Speed mattered for the same reason. People use a cache to save time. Finding, downloading and preparing a cached result all count towards the wait. A cache hit is only useful if reusing the result costs less time than doing the work again. I wanted to reduce that overhead, including the time spent saving work for the next build.
Built for sharing and reuse
When I think about an open cache platform, I mean being able to use it with the machines and build tools a team chooses. I want useful cache to be available wherever a compatible build needs it. I wanted to choose my machine without losing access to a compatible Ruby build.
The same idea applies to storage. If several builds contain the same data, I want them to reuse what is already stored instead of uploading and keeping another copy for each build. That is why content-addressed storage became part of BoringCache: identifying data by its contents gives those builds a way to refer to the same stored data.
The experiments began in July 2025. I launched the first iteration of BoringCache in July 2026. The scope had grown beyond Ruby, but the reason for building it was still the same. I wanted people to spend less time waiting for work that a compatible machine had already done, and to be able to reuse that work without requiring them to use the same provider for caching and compute.