14 - Caching : les bonnes réponses sont celles qu'on n'envoie pas
Ce que tu vas apprendre
- Les headers Cache-Control et leurs directives
- Le mecanisme ETag et les requêtes conditionnelles (304 Not Modified)
- La différence entre Last-Modified et ETag
- Le caching CDN et les stratégies d'invalidation
Prerequisites
Avoir lu l'article sur le rate limiting. Comprendre le fonctionnement basique d'une requête HTTP (headers, status codes).
J'avais une API qui renvoyait la meme liste de categories 200 fois par minute. 200 requêtes, 200 réponses identiques, 200 fois la meme requête SQL. Le jour ou j'ai ajoute un Cache-Control: max-age=300, le nombre de requêtes qui atteignaient le serveur est passe a une toutes les 5 minutes. Meme réponse, 99.5% de bande passante en moins.
Les niveaux de cache
Le caching HTTP fonctionne a plusieurs niveaux :
- Cache navigateur : le browser stocke la réponse localement
- Cache proxy/CDN : un intermediaire (Cloudflare, Fastly) stocke la réponse
- Cache applicatif : ton serveur cache les donnees en mémoire (Redis, in-memory)
Les deux premiers sont contrôles par les headers HTTP. Le troisieme, c'est ton code. Cet article se concentre sur les headers.
Cache-Control
Le header Cache-Control est le chef d'orchestre. Il dit au client et aux intermediaires ce qu'ils peuvent cacher et pendant combien de temps.
Cache-Control: public, max-age=3600
Les directives principales :
- public : tout le monde peut cacher (navigateur, CDN, proxys)
- private : seul le navigateur peut cacher (donnees spécifiques a un user)
- max-age=N : la réponse est valide pendant N secondes
- no-cache : tu peux stocker, mais revalide a chaque fois auprès du serveur
- no-store : ne stocke rien, jamais (donnees sensibles)
- s-maxage=N : comme max-age mais uniquement pour les caches partages (CDN)
typescript// Donnees publiques, cachees 1 heure
app.get("/categories", (req, res) => {
res.set("Cache-Control", "public, max-age=3600");
res.json(categories);
});
// Donnees privees, cache navigateur seulement
app.get("/users/me", (req, res) => {
res.set("Cache-Control", "private, max-age=60");
res.json(user);
});
// Donnees sensibles, jamais cachees
app.get("/users/me/billing", (req, res) => {
res.set("Cache-Control", "no-store");
res.json(billing);
});
ETag et requêtes conditionnelles
Le max-age a un problème : quand il expire, le client refait une requête complète, meme si la donnee n'a pas change. L'ETag resout ca.
Le serveur généré un identifiant unique pour chaque version de la ressource (souvent un hash du contenu) :
HTTP/1.1 200 OK
ETag: "a1b2c3d4"
Cache-Control: no-cache
{"name": "Burger", "price": 12}
A la prochaine requête, le client envoie l'ETag :
GET /products/42 HTTP/1.1
If-None-Match: "a1b2c3d4"
Si la ressource n'a pas change, le serveur répond 304 Not Modified sans body. Le client utilise sa copie locale. Zero bande passante gaspillee.
typescriptimport crypto from "crypto";
app.get("/products/:id", (req, res) => {
const product = getProduct(req.params.id);
const etag = crypto
.createHash("md5")
.update(JSON.stringify(product))
.digest("hex");
res.set("ETag", `"${etag}"`);
res.set("Cache-Control", "no-cache");
if (req.headers["if-none-match"] === `"${etag}"`) {
return res.status(304).end();
}
res.json(product);
});
Last-Modified
Alternative a l'ETag basee sur la date de modification :
HTTP/1.1 200 OK
Last-Modified: Sat, 29 Mar 2026 10:00:00 GMT
Le client envoie :
GET /products/42 HTTP/1.1
If-Modified-Since: Sat, 29 Mar 2026 10:00:00 GMT
Si la ressource n'a pas change depuis cette date, le serveur renvoie un 304. C'est plus simple qu'un ETag mais moins precis : deux modifications dans la meme seconde ne seront pas detectees. J'utilise Last-Modified pour les ressources qui changent rarement (fichiers statiques) et ETag pour les donnees dynamiques.
CDN caching
Un CDN comme Cloudflare ou Fastly se place entre tes clients et ton serveur. Il cache les réponses marquees public et les sert depuis ses edge servers, proches geographiquement du client. Ton serveur ne recoit meme pas la requête.
Cache-Control: public, s-maxage=3600, max-age=60
Ici, le CDN cache pendant 1 heure (s-maxage), mais le navigateur ne cache que 60 secondes (max-age). Quand le cache navigateur expire, le client interroge le CDN (rapide) plutot que ton serveur (lent).
Pour les pages d'un site comme paltemps.fr, le CDN cache les réponses des endpoints publics et les sert en quelques millisecondes, quel que soit le pays du visiteur.
L'invalidation : le vrai problème
Phil Karlton a dit qu'il n'y a que deux choses difficiles en informatique : nommer les choses et invalider le cache. Il avait raison.
Quand une ressource change, comment dire au cache de se mettre à jour ?
Stratégie 1 : TTL court. Tu mets un max-age de 60 secondes. Le cache se rafraichit naturellement. Simple, mais tu acceptes 60 secondes de donnees obsolètes.
Stratégie 2 : Purge explicite. Tu appelles l'API du CDN pour supprimer une entree :
typescriptawait fetch("https://api.cloudflare.com/client/v4/zones/{zone}/purge_cache", {
method: "POST",
headers: { Authorization: "Bearer {token}" },
body: JSON.stringify({ files: ["https://api.example.com/products/42"] })
});
Stratégie 3 : Cache-busting par version. Tu ajoutes un paramètre de version : /products/42?v=3. Chaque mise à jour incremente la version.
En pratique, je combine TTL court pour les listes et purge explicite pour les ressources individuelles quand la fraicheur est critique.
Vary : le cache qui sait distinguer
Le header Vary dit au cache que la réponse depend d'autres headers. Si ton API renvoie du JSON ou du XML selon Accept :
Vary: Accept, Accept-Encoding
Le cache stocke une version par combinaison de Accept et Accept-Encoding. Sans Vary, un client qui demande du XML pourrait recevoir la version JSON cachee pour un autre client.
Résumé
Cache-Controlcontrôle qui peut cacher et pendant combien de tempsETag+If-None-Matchevitent de renvoyer des donnees inchangees (304)Last-Modifiedest plus simple mais moins precis qu'un ETag- Le CDN cache les réponses
publicpres de tes clients - L'invalidation est le vrai defi : combine TTL, purge et cache-busting selon le cas
Varygarantit que le cache distingue les réponses selon les headers de la requête
Article précédent : Rate limiting Article suivant : Upload de fichiers