Retour au Blog

Réduire les coûts de stockage KV d'OpenNext sur Cloudflare Workers

2026-05-115 min read

Learn English Sounds a commencé à dépenser 0,50 $ par jour pour le stockage Cloudflare KV. Cela ne semble pas beaucoup jusqu'à ce que vous réalisiez que c'est pour un site au contenu relativement statique qui coûtait auparavant presque rien à héberger. J'ai creusé et trouvé un piège classique d'OpenNext sur Cloudflare. Si vous exécutez Next.js sur Workers via l'adaptateur @opennextjs/cloudflare, celui-ci vous affecte probablement aussi.

Qu'est-ce que Cloudflare KV ?

Cloudflare KV (pour Key-Value) est un magasin clé-valeur distribué mondialement, conçu pour les charges de travail à forte lecture. Vous écrivez une valeur une fois et elle est éventuellement répliquée dans des centres de données du monde entier, de sorte que les lectures sont rapides depuis n'importe où. La tarification comporte deux composantes : les opérations (lectures, écritures, suppressions) et le stockage (facturé par Go-mois). Le premier Go de stockage est inclus, puis il en coûte environ 0,50 $ par Go-mois supplémentaire. C'est ce deuxième chiffre qui vous piège lorsque quelque chose remplit silencieusement l'espace de noms avec des données obsolètes.

Pour OpenNext, KV est l'endroit où les pages rendues sont stockées afin que les requêtes futures n'aient pas à être re-rendues. C'est rapide, peu coûteux par opération, et idéal pour la mise en cache de HTML et JSON. Le piège est que rien dans KV n'expire automatiquement. Si vous écrivez une clé, elle reste jusqu'à ce que vous la supprimiez.

La configuration

Le site utilise OpenNext pour déployer Next.js sur Cloudflare Workers. Le cache incrémental (la couche de stockage derrière ISR et le cache de route complète) est configuré pour utiliser KV avec un wrapper de cache régional. La configuration pertinente ressemble à ceci :

import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import kvIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/kv-incremental-cache";
import { withRegionalCache } from "@opennextjs/cloudflare/overrides/incremental-cache/regional-cache";
import d1TagCache from "@opennextjs/cloudflare/overrides/tag-cache/d1-next-tag-cache";

export default defineCloudflareConfig({
  incrementalCache: withRegionalCache(kvIncrementalCache, {
    mode: "long-lived",
    bypassTagCacheOnCacheHit: true,
  }),
  tagCache: d1TagCache,
});

C'est exactement ce que recommandent les docs OpenNext. Les pages statiques sont rendues au moment de la compilation, écrites dans KV, et servies à partir d'une couche Cache API régionale à la sortie. L'utilisation du CPU diminue car le worker a rarement besoin de rendre quoi que ce soit. Le coût de stockage était censé être une erreur d'arrondi.

Qu'est-ce que le cache régional ?

Cela vaut la peine de s'y attarder, car cela explique pourquoi la suppression de KV ne nuit pas aux performances. Cloudflare donne à chaque worker accès à l'API Cache, un cache en mémoire par région qui réside dans le centre de données périphérique touché par la requête. withRegionalCache d'OpenNext encapsule le cache incrémental KV et utilise cette API Cache comme première couche.

L'ordre de recherche pour toute page mise en cache est : d'abord l'API Cache régionale, puis KV, puis le re-rendu. La première requête dans une région extrait la page de KV et la stocke dans le cache local. Toutes les requêtes ultérieures dans cette région sont servies depuis la mémoire en périphérie sans toucher à KV ni exécuter le code de rendu du worker. Avec mode: "long-lived", les copies régionales restent aussi longtemps que Cloudflare les conserve, ce qui, sur un nœud chaud, dure généralement de quelques minutes à quelques heures.

KV est donc un stockage à froid. Il n'est lu que lorsque le cache d'une région est froid ou évincé. La suppression des clés KV non référencées ne change pas la fréquence de lecture de KV ; elle ne change que la quantité que vous stockez. Le CPU ne bouge pas car rien dans le chemin critique n'a changé.

Ce que j'ai réellement trouvé dans KV

J'ai listé les clés dans l'espace de noms. Il y en avait 276 637. Chacune commençait par incremental-cache/. Le regroupement par le deuxième segment de chemin racontait l'histoire réelle :

$ wrangler kv key list --namespace-id <id> --remote \
    | jq -r '.[].name' | awk -F/ '{print $2}' \
    | sort | uniq -c | sort -rn | head

17896 Mqxc5oa6LB-nPqyGFrVbV
13068 zMp2Qi0YY-gVHmaXRB9Ce
 9108 zMlI9hb_vFtk705L8GDKL
 8748 -KcEM0U2hAvoQH7n5dyV5
  ...
 5895 W-RYs65XPM1m0Dg899-cz

Trente-neuf préfixes distincts. Chacun est un BUILD_ID de Next.js d'un déploiement précédent. Chaque next build génère un nouvel identifiant de build. Chaque déploiement écrit un nouvel univers d'entrées de cache délimitées par cet identifiant. Le worker actif ne lit jamais que le préfixe de son propre build. Les anciens préfixes sont un poids mort pur, et rien dans OpenNext ou Cloudflare ne les nettoie pour vous.

C'est ainsi qu'un site actif accumule silencieusement plus de 250 000 clés orphelines.

Pourquoi votre build actif grossit aussi

Il y a une deuxième tendance intéressante dans cette liste. Le préfixe le plus grand contenait 17 896 clés, le suivant 13 068, et la plupart des autres tournaient autour de 6 500. Le plus grand est le build actif actuel. Il a plus de clés que les autres car le trafic réel continue de remplir de nouvelles pages de longue traîne au fil du temps. C'est normal et sans problème. Le problème, c'est tout ce qui se trouve en dessous.

La correction

Le worker actif ne lit que depuis incremental-cache/<current-BUILD_ID>/. Donc, après chaque déploiement réussi, supprimez toutes les clés qui ne commencent pas par ce préfixe. Les clés que vous supprimez sont garanties d'être non référencées par aucun chemin de code actif. Ce n'est pas une invalidation de cache qui coûte du CPU ; c'est un nettoyage de déchets qui ne sont jamais lus.

Un script de nettoyage qui s'exécute après le déploiement

J'ai ajouté un script Node au projet et l'ai enchaîné dans la commande de déploiement. Il lit l'identifiant de build à partir de la sortie de build OpenNext, liste toutes les clés KV, et supprime en masse tout ce qui est en dehors du préfixe actif par blocs de 10 000 clés (la limite de suppression en masse de wrangler).

// scripts/prune-kv-cache.mjs
// ---------------------------------------------------------------
// Supprime les entrées de cache incrémental OpenNext obsolètes de Cloudflare KV
// après chaque déploiement réussi. À exécuter comme étape post-déploiement.
// ---------------------------------------------------------------

import { readFileSync, writeFileSync, mkdtempSync, rmSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { tmpdir } from "node:os";
import { join } from "node:path";

// Espace de noms Cloudflare KV qui alimente le cache incrémental d'OpenNext.
// Trouvez cet ID dans wrangler.toml sous [[kv_namespaces]] pour NEXT_INC_CACHE_KV.
const KV_NAMESPACE_ID = "<votre-id-espace-de-noms>";

// OpenNext écrit les clés de cache sous `incremental-cache/<BUILD_ID>/...`.
// Le worker actif ne lit que son propre BUILD_ID, donc tout autre préfixe est un déchet.
// Nous lisons le BUILD_ID fraîchement construit à partir du disque afin que le script corresponde toujours
// au déploiement qui vient d'avoir lieu.
const KEEP_BUILD_ID = readFileSync(".open-next/assets/BUILD_ID", "utf8").trim();

// `kv bulk delete` de wrangler accepte au maximum 10 000 clés par appel.
const CHUNK_SIZE = 10000;

// 1) Lister chaque clé dans l'espace de noms.
// maxBuffer est augmenté à 512 Mo car les gros sites peuvent renvoyer des dizaines de Mo de JSON.
const list = spawnSync(
  "npx",
  ["wrangler", "kv", "key", "list", "--namespace-id", KV_NAMESPACE_ID, "--remote"],
  { encoding: "utf8", maxBuffer: 1024 * 1024 * 512 }
);

// 2) Décider quoi garder et quoi supprimer.
// Garder :   incremental-cache/<KEEP_BUILD_ID>/...   (le worker actif lit à partir d'ici)
// Supprimer : incremental-cache/<autre chose>/...  (orphelin des déploiements précédents)
// Les clés non-cache sont laissées seules par la deuxième vérification `startsWith`.
const keepPrefix = `incremental-cache/${KEEP_BUILD_ID}/`;
const toDelete = JSON.parse(list.stdout)
  .map((k) => k.name)
  .filter((n) => n.startsWith("incremental-cache/") && !n.startsWith(keepPrefix));

// 3) Supprimer en masse les clés orphelines par blocs de 10k clés.
// wrangler lit chaque bloc à partir d'un fichier JSON sur disque, nous utilisons donc un répertoire temporaire
// et le nettoyons à la fin, qu'il y ait succès ou échec.
const workDir = mkdtempSync(join(tmpdir(), "kv-prune-"));
for (let i = 0; i < toDelete.length; i += CHUNK_SIZE) {
  const chunkPath = join(workDir, `chunk_${i}.json`);
  // wrangler attend un tableau JSON de noms de clés, par exemple ["clé1", "clé2", ...]
  writeFileSync(chunkPath, JSON.stringify(toDelete.slice(i, i + CHUNK_SIZE)));
  spawnSync(
    "npx",
    ["wrangler", "kv", "bulk", "delete", "--namespace-id", KV_NAMESPACE_ID, "--remote", chunkPath],
    { stdio: "inherit" } // diffuse la barre de progression de wrangler directement dans le terminal
  );
}

// 4) Nettoyer le répertoire temporaire et rapporter ce que nous avons fait.
rmSync(workDir, { recursive: true, force: true });
console.log(`Supprimé ${toDelete.length} clés de cache obsolètes.`);

Ensuite, je l'ai intégré dans la chaîne de déploiement dans package.json pour qu'il s'exécute à chaque fois, automatiquement :

"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy --cacheChunkSize 1000 && npm run prune-kv-cache",
"prune-kv-cache": "node scripts/prune-kv-cache.mjs"

L'ordre est important. Construire d'abord pour que le nouveau BUILD_ID existe sur le disque. Déployer ensuite pour que le nouveau worker soit actif et lise à partir du nouveau préfixe. Nettoyer enfin pour que l'ancien préfixe puisse être supprimé en toute sécurité dès que le basculement est terminé.

Le résultat

Sur le site que j'ai nettoyé, KV est passé de 276 637 clés réparties sur 39 builds à 7 619 clés dans le seul build actif, soit une réduction d'environ 97 %. Le coût de stockage quotidien projeté est passé de 0,50 $ à quelques centimes. Aucune modification du chemin de code, aucune modification de la stratégie de mise en cache, aucune régression du CPU. Le cache régional enveloppe toujours KV exactement comme avant, de sorte que les lectures fréquentes proviennent de l'API Cache et ne touchent jamais le stockage.

Quand ne pas nettoyer

Le nettoyage est la bonne approche par défaut pour les déploiements à version unique, mais il existe des situations réelles où il peut vous nuire.

Rétrogradation instantanée comme filet de sécurité. Si vous comptez sur wrangler rollback pour revenir à une version précédente du worker en quelques secondes, le cache de la version précédente disparaît après un nettoyage. La rétrogradation fonctionne toujours, mais la première vague de requêtes après la rétrogradation manquera toutes et sera re-rendue. Le CPU augmente pendant quelques minutes. Modifiez le script pour conserver les N derniers identifiants de build au lieu d'un seul actif si cela est important.

Mises en production progressives ou déploiements canaris. Si des mises en production progressives Cloudflare ou tout type de déploiement graduel sont en jeu, plusieurs versions de worker servent du trafic en même temps. Chacune a besoin de son propre préfixe de build intact. Ne nettoyez pas tant que le déploiement n'atteint pas 100 %.

Requêtes en cours pendant le déploiement. Il y a toujours une courte fenêtre où l'ancien worker peut encore servir quelques retardataires juste au moment où le nouveau démarre. Ils manqueront le cache et seront re-rendus une fois. Pour la plupart des sites, c'est invisible. Il est bon de le savoir si vous gérez du trafic par rafales.

Une note sur R2 comme alternative

OpenNext propose également un cache incrémental basé sur R2. Le stockage R2 est environ 33 fois moins cher que KV (environ 0,015 $ contre 0,50 $ par Go-mois au moment de la rédaction), et le wrapper de cache régional se place devant lui de la même manière qu'il se place devant KV. Ainsi, les performances de votre chemin critique ne changent pas, mais le stockage à froid devient considérablement moins cher. Le nettoyage a toujours du sens pour l'hygiène, mais la pression sur les coûts disparaît en grande partie. Si votre site est suffisamment grand pour que même le cache du build actif soit conséquent, passer à R2 vaut la peine d'être examiné.

Restez Informé

Recevez les derniers articles et analyses directement dans votre boîte de réception.

Unsubscribe anytime. No spam, ever.