
Sixty containers on one server
One bare-metal box runs dozens to hundreds of Hoody containers. KSM and BTRFS dedup make the marginal cost near zero.
The first job in your pipeline tars node_modules and pipes it to a Hoody URL. Twenty downstream jobs curl that same URL and untar. With ?n=20 the producer waits for all twenty workers to connect, then streams once — fanned out to all of them. No S3 bucket, no cache action, no egress bill.
There is no client library, no daemon, no SDK. The producer streams its tarball into PUT /pipe/cache?n=20. Each worker streams it back out of GET /pipe/cache?n=20. The pipe holds the bytes only while they're in flight — never on disk.
tar packs node_modules; zstd compresses on the fly; curl PUTs the bytes straight into the pipe path. No tempfile, no upload-artifact step, no bucket credentials.
Each test worker GETs the same URL, decompresses, and untars into its working directory. Slow workers apply backpressure to the producer but don't block faster ones.
?n=20One query parameter. Producer and consumer agree on the same n. The pipe holds the upload until exactly that many receivers are connected, then opens the floodgate.
CI caching used to be a tax: storage you'll overwrite tomorrow, egress every time a worker pulls, engineering time on the cache action's quirks. The pipe deletes all three line items at once.
There is no S3 bucket because there is no storage. The pipe forgets the bytes the second the transfer ends, so there is nothing to charge per-GB-month or per-GB-out for. The cache stops being a line item.
No yaml-encoded keys, no save-cache / restore-cache split, no debugging why a cache hit didn't happen on the right runner. Just curl. The same two lines run on GitHub Actions, BuildKite, Jenkins, your laptop, or a cron container.
Branches just use different paths. /pipe/cache/main, /pipe/cache/feat-x, /pipe/cache/PR-742. Nothing to invalidate. Nothing to evict. When the branch dies, its path stops being asked for and that is the entire lifecycle.
On a real workload — node_modules around 800 MB, twenty parallel test workers, a hundred CI runs a day — most of the bill is egress, not storage.
20× egress
Each of the twenty workers pulls the cache out of S3. Twenty downloads of an 800 MB tarball is 16 GB of egress per CI run. The bucket itself is the easy part — the egress is what compounds.
1× transfer
The producer streams the 800 MB once. The pipe fans the bytes out to all twenty receivers in flight. One transfer through the wire, no per-receiver multiplier, no storage bill.
Numbers are illustrative for a typical Node monorepo cache. Actual savings depend on tarball size, worker fan-out, and the egress price your provider charges out of the cache region. The shape — linear vs. constant in worker count — is invariant.
The cache layer is HTTP. It always was. We just hadn't noticed.
Caches were never about storage. They were about getting the same bytes to N workers without rebuilding. HTTP already does that — once you let one URL fan out to a known number of receivers. The bucket was a workaround for the fan-out we didn't have.
One PUT, n GETs, identical bytes. Backpressure is per-receiver so the slow worker doesn't slow the fast ones.
PUT /pipe/cache?n=20The pipe holds the bytes for the transfer and forgets them when it ends. There is nothing to evict, nothing to lifecycle, nothing to back up.
TTL ≤ 5 min · then evictedEach branch picks its own path. No shared keyspace, no collisions. The path is the cache key and the URL at once.
/pipe/cache/[branch]Most CI caches solve the same problem: get the same tarball to N workers. They do it through storage and egress. The pipe does it through the wire.
Two curl commands. One URL. Twenty workers fed.