
Soixante conteneurs sur un seul serveur
Une machine bare-metal exécute des dizaines à des centaines de conteneurs Hoody. La dédupplication KSM et BTRFS rend le coût marginal quasi nul.
Le premier job de votre pipeline tar node_modules et le pipe vers une URL Hoody. Vingt jobs en aval font curl sur la même URL et untar. Avec ?n=20, le producteur attend que les vingt workers se connectent, puis diffuse une fois — fanné à tous. Pas de bucket S3, pas d'action de cache, pas de facture d'egress.
Pas de bibliothèque cliente, pas de daemon, pas de SDK. Le producteur diffuse son tarball dans PUT /pipe/cache?n=20. Chaque worker le diffuse à la sortie de GET /pipe/cache?n=20. Le pipe ne retient les octets que tant qu'ils sont en vol — jamais sur disque.
tar empaquette node_modules ; zstd compresse à la volée ; curl PUT les octets directement dans le chemin du pipe. Pas de fichier temporaire, pas d'étape upload-artifact, pas de credentials de bucket.
Chaque worker de test fait GET sur la même URL, décompresse, et untar dans son répertoire de travail. Les workers lents appliquent la backpressure au producteur mais ne bloquent pas les plus rapides.
?n=20Un seul paramètre de requête. Producteur et consommateur s'accordent sur le même n. Le pipe retient l'upload jusqu'à ce qu'exactement ce nombre de récepteurs soit connecté, puis ouvre les vannes.
La mise en cache CI était une taxe : du stockage que vous écraserez demain, de l'egress chaque fois qu'un worker tire, du temps d'ingénierie sur les caprices de l'action de cache. Le pipe supprime les trois lignes de facture d'un coup.
Pas de bucket S3 parce qu'il n'y a pas de stockage. Le pipe oublie les octets à la seconde où le transfert se termine, donc rien à facturer par Go-mois ou par Go-sortant. Le cache cesse d'être une ligne de facture.
Pas de clés encodées en yaml, pas de scission save-cache / restore-cache, pas de débogage pour comprendre pourquoi un cache hit ne s'est pas produit sur le bon runner. Juste curl. Les deux mêmes lignes tournent sur GitHub Actions, BuildKite, Jenkins, votre laptop, ou un conteneur cron.
Les branches utilisent simplement des chemins différents. /pipe/cache/main, /pipe/cache/feat-x, /pipe/cache/PR-742. Rien à invalider. Rien à évincer. Quand la branche meurt, son chemin n'est plus demandé et c'est tout le cycle de vie.
Sur une charge réelle — node_modules autour de 800 Mo, vingt workers de test parallèles, une centaine de runs CI par jour — la majeure partie de la facture est de l'egress, pas du stockage.
20× egress
Chacun des vingt workers tire le cache hors de S3. Vingt téléchargements d'un tarball de 800 Mo, c'est 16 Go d'egress par run CI. Le bucket lui-même est la partie facile — c'est l'egress qui s'accumule.
1× transfert
Le producteur diffuse les 800 Mo une fois. Le pipe fanne les octets aux vingt récepteurs en vol. Un seul transfert sur le câble, pas de multiplicateur par récepteur, pas de facture de stockage.
Les chiffres sont illustratifs pour un cache de monorepo Node typique. Les économies réelles dépendent de la taille du tarball, du fan-out de workers, et du prix d'egress que votre fournisseur facture hors de la région du cache. La forme — linéaire vs. constante en nombre de workers — est invariante.
La couche de cache, c'est HTTP. Ça l'a toujours été. On ne l'avait juste pas remarqué.
Les caches n'ont jamais été une question de stockage. Ils étaient une question d'amener les mêmes octets à N workers sans reconstruire. HTTP fait déjà ça — une fois qu'on laisse une URL faire fan-out vers un nombre connu de récepteurs. Le bucket était un contournement pour le fan-out qu'on n'avait pas.
Un PUT, n GET, octets identiques. La backpressure est par récepteur donc le worker lent ne ralentit pas les rapides.
PUT /pipe/cache?n=20Le pipe retient les octets pour le transfert et les oublie quand il se termine. Rien à évincer, rien à passer en cycle de vie, rien à sauvegarder.
TTL ≤ 5 min · puis évincéChaque branche choisit son propre chemin. Pas d'espace de clés partagé, pas de collisions. Le chemin est la clé de cache et l'URL en même temps.
/pipe/cache/[branch]La plupart des caches CI résolvent le même problème : amener le même tarball à N workers. Ils le font via stockage et egress. Le pipe le fait via le câble.
Deux commandes curl. Une URL. Vingt workers nourris.