Le reverse proxy — gardien de porte
Terminaison SSL, compression, cache, load balancing — un seul point d'entrée qui fait tout.
Imagine que ton application backend soit un restaurant en cuisine. Les clients n'y entrent jamais directement — ils passent par un maître d'hôtel (le reverse proxy) qui distribue les tables, gère les réservations, et peut même réchauffer des plats déjà préparés (le cache). C'est exactement le rôle d'un reverse proxy : s'interposer entre Internet et tes serveurs d'application.
Serveur qui reçoit les requêtes des clients au nom des serveurs d'origine. Il est transparent pour le client — celui-ci croit parler directement à l'application.
Un reverse proxy cumule plusieurs responsabilités :
| Fonction | Ce que ça fait |
|---|---|
| Terminaison SSL/TLS | Déchiffre HTTPS en amont, parle HTTP en clair à l'upstream. Ton app n'a plus à gérer les certificats. |
| Load balancing | Distribue le trafic entre plusieurs serveurs upstream (round-robin, least-connections, IP hash…). |
| Compression | Applique gzip/brotli à la volée avant d'envoyer au client, sans toucher à l'application. |
| Cache HTTP | Stocke les réponses upstream et les resert sans contacter l'origine. |
| Rate limiting | Limite le nombre de requêtes par IP/seconde avant même d'atteindre l'app. |
| Headers proxy | Ajoute X-Forwarded-For, X-Real-IP pour que l'upstream connaisse la vraie IP client. |
Forward proxy vs Reverse proxy
La confusion est fréquente. Retiens une règle simple :
Côté client. Le client le configure explicitement (ex : proxy d'entreprise). Il cache pour le client — les réponses que plusieurs clients demandent souvent.
Côté serveur. Le client ne sait pas qu'il existe. Il cache pour le serveur — soulage l'origin des requêtes identiques.
(Nginx, HAProxy, Caddy)
Les headers proxy importants
Quand le reverse proxy contacte l'upstream, il ajoute des headers pour transmettre l'information sur le client d'origine :
# Headers ajoutés par le reverse proxy à la requête upstream X-Forwarded-For: 203.0.113.42, 10.0.0.1 # IP réelle client + chaîne de proxys X-Real-IP: 203.0.113.42 # IP réelle client (simplifiée) X-Forwarded-Proto: https # Protocole original (HTTPS) X-Forwarded-Host: www.exemple.com # Host original du client Connection: keep-alive # Pool de connexions vers upstream
Simulateur — Routing reverse proxy
Clique sur "Envoyer une requête" pour voir le proxy distribuer le trafic entre ses deux upstreams en round-robin.
Cache-Control — le langage de la fraîcheur
max-age, s-maxage, public, private, no-store — chaque directive dit aux caches combien de temps croire une réponse.
Le header Cache-Control est le principal mécanisme de contrôle du cache HTTP. Il est envoyé dans la réponse du serveur et indique à tous les caches (browser, reverse proxy, CDN) combien de temps ils peuvent conserver la ressource et dans quelles conditions.
Header HTTP de réponse (et de requête) qui gouverne le comportement des caches. Introduit en HTTP/1.1, il remplace le header legacy
Expires.
Les directives essentielles
| Directive | Qui ça concerne | Signification |
|---|---|---|
max-age=N |
Tous les caches | La ressource est fraîche pendant N secondes après la réponse. |
s-maxage=N |
Caches partagés (CDN, proxy) uniquement | Remplace max-age pour les caches partagés. Le browser ignore s-maxage. |
public |
Caches partagés | Autorise la mise en cache même si la requête était authentifiée. |
private |
Browser seulement | Interdit aux caches partagés. Typique pour les réponses personnalisées. |
no-store |
Tous | Ne jamais stocker. Rien n'est mis en cache, nulle part. |
no-cache |
Tous | Peut être stocké, mais doit être revalidé auprès du serveur avant chaque utilisation. |
must-revalidate |
Tous | Une fois expiré, interdit de servir la version stale. Doit obligatoirement contacter le serveur. |
stale-while-revalidate=N |
Tous (support variable) | Sert la version stale pendant N secondes pendant qu'une revalidation se fait en arrière-plan. |
stale-if-error=N |
Tous | Si l'upstream renvoie une erreur 5xx, sert le stale pendant N secondes. |
immutable |
Browser | Tant que max-age n'est pas écoulé, ne jamais revalider — même si l'utilisateur rafraîchit. |
max-age vs s-maxage
C'est la distinction la plus importante à maîtriser. Prenons un exemple concret :
# Une page HTML dynamique servie via CDN Cache-Control: public, max-age=60, s-maxage=86400
Ce que ça signifie :
- Le browser garde la page 60 secondes (1 minute). Rapide à rafraîchir pour l'utilisateur.
- Le CDN garde la page 86 400 secondes (24 heures). L'origin n'est contacté qu'une fois par jour par PoP.
s-maxage ne s'applique qu'aux caches partagés. Un browser ne regarde que max-age (sauf private qui lui indique explicitement "c'est pour toi seulement").
Recettes courantes
# Asset statique avec hash dans l'URL (ex: main.a1b2c3.js) Cache-Control: public, max-age=31536000, immutable # 1 an, jamais revalidé — le hash change = nouvelle URL = nouveau cache # Page HTML qui peut changer (CDN cache longtemps, browser cache peu) Cache-Control: public, max-age=60, s-maxage=86400, stale-while-revalidate=3600 # API JSON personnalisée (profil utilisateur) Cache-Control: private, max-age=300 # Browser peut cacher 5 min, CDN jamais # Données sensibles (panier, paiement) Cache-Control: no-store # Aucun cache nulle part # Ressource toujours fraîche côté browser (SPA entry point) Cache-Control: no-cache # Revalidation systématique — 304 si rien n'a changé
no-cache dit "tu peux stocker, mais redemande à chaque fois". no-store dit "ne stocke rien du tout". C'est le piège le plus classique de Cache-Control.
Le header Expires (héritage)
Avant Cache-Control, HTTP/1.0 utilisait le header Expires avec une date absolue :
Expires: Wed, 01 Jan 2026 00:00:00 GMT
Problème : si l'horloge du serveur et du client diffèrent, le TTL est faussé. Cache-Control: max-age utilise des secondes relatives — c'est toujours lui qui prime quand les deux sont présents. N'utilise plus Expires dans un nouveau projet.
ETag et Last-Modified — la revalidation conditionnelle
Plutôt que re-télécharger, le client demande « as-tu changé ? » — 304 Not Modified économise la bande passante.
Le cache HTTP a deux modes de fonctionnement : la fraîcheur (la ressource est valide pendant N secondes) et la revalidation (le client vérifie si la ressource a changé avant de l'utiliser). La revalidation conditionnelle est le mécanisme qui permet d'économiser de la bande passante sans sacrifier la fraîcheur.
Le cache stocke la ressource ET son empreinte (ETag ou date de modification). Quand la ressource expire, le client envoie l'empreinte au serveur — si rien n'a changé, le serveur répond
304 Not Modified sans corps.
ETag
Un ETag (Entity Tag) est une empreinte de la ressource — un hash du contenu, de la date de modification, ou tout autre identifiant opaque. Le serveur le génère, le client le stocke et le renvoie.
# 1. Première réponse — le serveur inclut un ETag HTTP/1.1 200 OK ETag: "abc123def456" Cache-Control: max-age=3600 [corps de 45 Ko] # 2. Une heure plus tard — le cache expire, le client revalide GET /api/produits If-None-Match: "abc123def456" # 3a. Rien n'a changé — 304, pas de corps (0 Ko transféré !) HTTP/1.1 304 Not Modified ETag: "abc123def456" # 3b. La ressource a changé — 200 avec le nouveau contenu HTTP/1.1 200 OK ETag: "xyz789" [nouveau corps de 47 Ko]
ETag fort vs ETag faible
| Type | Syntaxe | Garantie |
|---|---|---|
| ETag fort | "abc123" |
Octets identiques. Deux réponses avec le même ETag fort sont bit-à-bit identiques. |
| ETag faible | W/"abc123" |
Représentation équivalente (même sens, pas forcément mêmes octets). Ex: HTML recompressé différemment. |
If-Match pour du verrouillage optimiste.
Last-Modified
Mécanisme similaire mais basé sur une date. Moins précis que l'ETag (résolution à la seconde), mais universellement supporté.
# Réponse initiale Last-Modified: Wed, 01 Jan 2025 12:00:00 GMT Cache-Control: max-age=3600 # Revalidation par le client If-Modified-Since: Wed, 01 Jan 2025 12:00:00 GMT # Réponse si non modifié HTTP/1.1 304 Not Modified
If-Match — verrouillage optimiste
L'ETag sert aussi à éviter les conflits d'édition. Imagine deux utilisateurs qui modifient la même ressource simultanément :
# 1. Alice et Bob lisent la ressource en même temps GET /articles/42 # → ETag: "v1" # 2. Alice soumet sa modification en premier PUT /articles/42 If-Match: "v1" # → 200 OK, ETag: "v2" # 3. Bob tente de soumettre — mais la ressource a changé PUT /articles/42 If-Match: "v1" # → 412 Precondition Failed — Bob doit récupérer la version d'Alice d'abord
If-None-Match = économie de bande passante (cache). ETag + If-Match = protection contre les conflits d'édition concurrente.
Vary — le cache multidimensionnel
Vary: Accept-Encoding indique au cache que la même URL peut avoir des représentations différentes selon les headers.
Par défaut, un cache identifie une ressource par son URL. Mais que se passe-t-il si la même URL peut renvoyer des réponses différentes selon les headers du client ? C'est là qu'intervient Vary.
Header de réponse qui indique au cache quels headers de requête doivent faire partie de la clé de cache.
Vary: Accept-Encoding signifie : "la même URL peut avoir plusieurs versions en cache, une par valeur d'Accept-Encoding".
Le problème sans Vary
Imagine ce scénario :
- Alice (navigateur moderne) envoie
Accept-Encoding: gzip, br. Le serveur répond avec la version brotli compressée. Le cache stocke cette réponse associée à l'URL. - Bob (client léger) envoie
Accept-Encoding: identity. Le cache lui sert la version brotli — qu'il ne sait pas décompresser. Contenu corrompu.
La solution : le serveur ajoute Vary: Accept-Encoding. Le cache crée alors deux entrées distinctes pour la même URL, une par valeur de Accept-Encoding.
# Réponse du serveur pour un asset compressé HTTP/1.1 200 OK Content-Encoding: br Vary: Accept-Encoding Cache-Control: public, max-age=31536000, immutable
Cas d'usage courants
| Vary: … | Pourquoi |
|---|---|
Accept-Encoding |
Versions compressées (gzip, br) vs non compressées en cache séparé. |
Accept-Language |
Contenu traduit selon la langue préférée du client. |
Accept |
API qui sert JSON ou XML selon le header Accept. |
Origin |
Réponses CORS différentes selon l'origine de la requête. |
Cache-Control: private.
Vary et les CDNs — la cache key
Les CDNs permettent souvent de modifier la cache key indépendamment du header Vary. Par exemple, Cloudflare peut ignorer certains query params ou normaliser Accept-Encoding avant de calculer la clé. Cela permet d'éviter la fragmentation excessive du cache :
# Sans normalisation CDN : # Accept-Encoding: gzip → entrée cache A # Accept-Encoding: gzip, br → entrée cache B # Accept-Encoding: gzip, deflate → entrée cache C # (toutes identiques dans la pratique !) # Avec normalisation CDN → une seule entrée, ratio de cache bien meilleur
CDN — les caches géographiques
Points of Presence, anycast, cache key — comment le contenu arrive depuis le PoP le plus proche plutôt que ton datacenter.
Un CDN (Content Delivery Network) est un réseau mondial de serveurs de cache. Plutôt que de livrer chaque réponse depuis ton datacenter unique, le CDN répond depuis un serveur proche du client — à quelques millisecondes au lieu de centaines.
Réseau de points de présence (PoP) géographiquement distribués qui mettent en cache et servent le contenu de ton origin. Le CDN agit comme un reverse proxy cache à l'échelle planétaire.
Points of Presence et anycast routing
Un PoP (Point of Presence) est un datacenter CDN, souvent colocalisé dans les grands échangeurs Internet mondiaux. Cloudflare en a 300+, Fastly 90+.
(CDN Edge)
Le routing anycast fait que l'adresse IP du CDN est annoncée depuis tous les PoPs simultanément. Le réseau BGP route automatiquement la requête vers le PoP le plus proche — sans que le client ait à savoir lequel.
Origin pull vs Origin push
Le CDN ne récupère la ressource que sur la première requête (MISS). Les suivantes sont servies depuis le cache (HIT). Mode par défaut, idéal pour le contenu dynamique ou mal connu à l'avance.
Tu envoies le contenu au CDN à l'avance (via API ou rsync). Aucune requête MISS possible. Idéal pour les assets de déploiement — zéro latence dès la première requête mondiale.
La cache key CDN
Par défaut, la cache key d'un CDN est l'URL complète (scheme + host + path + query string). Mais les CDNs permettent de la personnaliser :
- Strip cookies : si la réponse ne varie pas selon les cookies, les ignorer évite la fragmentation.
- Strip/normaliser query params :
/page?utm_source=twitter&utm_medium=social→ même cache que/page. - Ajouter des headers : inclure l'en-tête
Accept-Languagedans la clé pour du contenu multilingue.
Headers de diagnostic CDN
# Cloudflare CF-Cache-Status: HIT # HIT, MISS, EXPIRED, REVALIDATED, DYNAMIC, BYPASS CF-Ray: abc123-CDG # ID de requête + code IATA du PoP (CDG = Paris) Age: 3542 # Secondes depuis que l'entrée a été mise en cache # Varnish / Nginx upstream cache X-Cache: HIT X-Cache-Hits: 12 # Fastly X-Served-By: cache-cdg22-CDG X-Cache: HIT X-Cache-Hits: 5
Shield — mid-tier caching
Le shield (ou "origin shield") est un PoP CDN intermédiaire, situé entre les PoPs edge et ton origin. Quand 300 PoPs reçoivent simultanément un MISS, sans shield ils envoient tous 300 requêtes à ton origin. Avec un shield, ils n'en envoient qu'une seule au shield, qui lui-même n'en envoie qu'une seule à l'origin.
(mid-tier)
Les couches de cache — de l'edge à la BDD
Browser → Reverse proxy → CDN Edge → CDN Shield → App cache — chaque couche protège celle derrière.
Une requête HTTP en production traverse souvent 4 à 6 couches de cache avant d'atteindre la base de données. Chaque couche a ses propres caractéristiques : portée (privée ou partagée), protocole de validation, TTL, et coût de miss.
La pile complète
| # | Couche | Portée | Protocole | TTL typique |
|---|---|---|---|---|
| L1 | Browser cache | Privée (un user) | HTTP (max-age, ETag) | Minutes à 1 an |
| L2 | Reverse proxy / Nginx cache | Partagée (tous users) | HTTP (s-maxage) | Minutes à heures |
| L3 | CDN Edge (PoP) | Partagée (région) | HTTP (s-maxage, CF-Cache-Status) | Heures à jours |
| L4 | CDN Shield (mid-tier) | Partagée (global) | HTTP interne CDN | Heures à jours |
| L5 | Application cache (Redis) | Partagée (app servers) | API Redis/Memcached | Secondes à heures |
| L6 | OPcache PHP / Query cache DB | Locale (process) | Interne runtime | Durée du processus |
Lecture d'un cache miss en cascade
Quand aucune couche n'a la réponse (cache cold, après déploiement), la requête traverse tout :
- Browser : MISS → envoie la requête vers le CDN Edge.
- CDN Edge : MISS → contacte le CDN Shield.
- CDN Shield : MISS → contacte le reverse proxy.
- Reverse proxy : MISS → contacte l'application PHP.
- Application PHP : OPcache HIT, mais Redis MISS → contacte la BDD.
- BDD : requête SQL → résultat retourné.
- Chaque couche stocke la réponse au retour → les prochaines requêtes sont HIT.
L1–L4 vs L5 — deux paradigmes différents
Basé sur les headers HTTP. Transparent pour l'application. Le cache sait quand revalider grâce à ETag et Last-Modified. Invalidation par URL ou cache tags via API CDN.
Piloté par le code applicatif. La logique de TTL et d'invalidation est dans ton code PHP. Plus flexible, mais plus de responsabilité. Utile pour les objets sérialisés (résultats de requêtes complexes, données calculées).
Invalidation — le problème difficile
Il y a deux problèmes difficiles en informatique : l'invalidation de cache et le nommage. Voici comment résoudre le premier.
Phil Karlton a dit : "Il y a deux problèmes difficiles en informatique : l'invalidation de cache et le nommage des choses." La difficulté vient du fait qu'un cache par définition ne sait pas quand son contenu est devenu invalide — c'est à toi de le lui dire.
Processus par lequel une entrée de cache est marquée comme invalide (devant être supprimée ou revalidée) avant l'expiration de son TTL. Nécessaire quand les données sources ont changé.
Stratégie 1 — TTL expiration (passive)
La plus simple : attendre que le TTL expire naturellement. Pas d'invalidation active, pas d'API à appeler.
# Page de blog mise à jour toutes les heures maximum Cache-Control: public, max-age=3600 # Au pire, les visiteurs voient une version vieille d'1 heure. Acceptable.
Avantage : zéro infrastructure d'invalidation. Inconvénient : les changements ne sont pas visibles immédiatement.
Stratégie 2 — Versioning d'URL (cache busting)
Inclure un hash du contenu dans l'URL. Le contenu change → l'URL change → le cache considère que c'est une nouvelle ressource.
# Webpack / Vite génèrent automatiquement ces URLs /assets/main.a1b2c3d4.js # CSS original /assets/main.e5f6g7h8.js # Après modification → nouvelle URL # Header pour ces assets : durée maximale, jamais revalider Cache-Control: public, max-age=31536000, immutable
L'ancien fichier peut rester en cache indéfiniment — personne ne le demandera plus. La nouvelle URL est un MISS partout, mais elle est vite mise en cache.
Stratégie 3 — Cache tags (purge sélective)
Associer des "tags" à des entrées de cache, puis purger toutes les entrées d'un tag d'un coup.
# Réponse du serveur avec des tags cache (Cloudflare Cache-Tag) Cache-Tag: article-42, auteur-7, categorie-tech # Quand l'article 42 est modifié → purger le tag # API Cloudflare POST /zones/{zone_id}/purge_cache { "tags": ["article-42"] } # → Toutes les URLs cachées avec ce tag sont purgées instantanément
Utilisé par Varnish (avec Surrogate-Key), Cloudflare (Cache-Tag), Fastly (Surrogate-Key).
Stratégie 4 — Purge par URL
# Invalider une URL précise via l'API CDN POST /zones/{zone_id}/purge_cache { "files": ["https://exemple.com/articles/mon-article"] } # Déclenché automatiquement au déploiement ou après une édition CMS
Stratégie 5 — Event-driven invalidation
Le CMS ou l'application envoie un webhook au CDN dès qu'une ressource change. Invalidation quasi-instantanée, sans avoir à gérer des TTLs courts.
Simulateur — Stratégies d'invalidation
Sélectionne un type de ressource pour voir la stratégie d'invalidation adaptée, puis simule un changement.
stale-while-revalidate — fraîcheur sans attente
Sers la version stale pendant que tu revalides en background — aucun visiteur n'attend le serveur d'origine.
L'approche classique du cache HTTP a un défaut : quand une ressource expire, le prochain visiteur attend que le serveur réponde avant de recevoir quoi que ce soit. Sur un site à fort trafic, cette latence est visible. stale-while-revalidate résout ce problème élégamment.
Directive Cache-Control qui autorise le cache à servir une ressource expirée (stale) pendant N secondes, tout en déclenchant une revalidation en arrière-plan. Le visiteur reçoit une réponse instantanée, la prochaine version fraîche est prête pour le visiteur suivant.
Sans vs avec stale-while-revalidate
- Ressource expire.
- Visiteur A arrive → cache bloqué → attend le serveur → reçoit la réponse fraîche.
- Visiteur A attend 200–500 ms de latence origin.
- Ressource expire.
- Visiteur A arrive → cache sert immédiatement la version stale.
- En parallèle, le cache contacte le serveur en background.
- Visiteur B (quelques ms plus tard) reçoit déjà la version fraîche.
Exemple concret
# Page d'accueil : fraîche 1 min, stale acceptable pendant 5 min Cache-Control: public, max-age=60, stale-while-revalidate=300 # Comportement : # t=0 : mise en cache (fraîche) # t=59s : encore fraîche → servie directement # t=61s : stale → servie instantanément + revalidation en background # t=361s : stale trop vieux → revalidation bloquante (comme avant)
stale-if-error — résilience aux pannes
La directive sœur stale-if-error résout un problème différent : si l'origin tombe, servir le stale plutôt qu'une erreur 502/503.
# Si l'origin est down, sert la version en cache jusqu'à 24h Cache-Control: public, max-age=3600, stale-if-error=86400 # Combinaison robuste pour un site à fort SLA Cache-Control: public, max-age=60, stale-while-revalidate=300, stale-if-error=86400
stale-while-revalidate et stale-if-error sont très bien supportés par les CDNs modernes (Cloudflare, Fastly, Varnish). Le support browser est bon (Chrome, Firefox) mais l'implémentation varie. En pratique, c'est surtout le comportement CDN qui compte pour les performances globales.
must-revalidate — l'opposé strict
Si tu ne veux pas de comportement stale du tout, must-revalidate interdit explicitement de servir une ressource expirée sans avoir contacté le serveur. Utile pour des données critiques (stocks, prix, disponibilités).
# Prix d'un produit — jamais de stale Cache-Control: public, max-age=300, must-revalidate # Si le serveur est down et que les 300s sont écoulées → 504 Gateway Timeout # Pas de stale servi. Le client voit une erreur plutôt qu'un prix périmé.
Nginx — configurer le cache proxy
proxy_cache_path, proxy_cache_valid, X-Cache-Status — configurer Nginx comme reverse proxy cache en 20 lignes.
Nginx peut agir comme un reverse proxy cache complet. Il stocke les réponses upstream sur disque, les sert directement aux clients sans contacter l'application PHP, et expose un header de diagnostic pour que tu saches ce qui se passe à chaque requête.
Configuration de base
# nginx.conf — définir la zone de cache proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=app_cache:10m # nom de la zone + 10 Mo pour les clés en RAM max_size=1g # taille max du cache sur disque inactive=60m # supprime les entrées non demandées depuis 60 min use_temp_path=off; # écrit directement dans le répertoire final server { location / { proxy_pass http://php_upstream; proxy_cache app_cache; # TTLs par code de statut HTTP proxy_cache_valid 200 1h; # Réponses 200 : cache 1 heure proxy_cache_valid 301 1d; # Redirections permanentes : 1 jour proxy_cache_valid 404 1m; # 404 : 1 minute (évite les repeated misses) # Servir le stale si upstream en erreur ou en cours de mise à jour proxy_cache_use_stale error timeout updating http_500 http_502 http_503; # Header de diagnostic — affiche HIT, MISS, EXPIRED, BYPASS, STALE, UPDATING add_header X-Cache-Status $upstream_cache_status; } }
Les valeurs de $upstream_cache_status
| Valeur | Signification |
|---|---|
HIT | Réponse servie depuis le cache Nginx. L'upstream n'a pas été contacté. |
MISS | Pas en cache. L'upstream a été contacté, la réponse est maintenant stockée. |
EXPIRED | En cache mais TTL expiré. L'upstream a été contacté pour revalider. |
BYPASS | Cache ignoré (ex: requête avec Cookie, ou proxy_cache_bypass actif). |
STALE | Version expirée servie car proxy_cache_use_stale était actif et l'upstream en erreur. |
UPDATING | Version stale servie pendant qu'une revalidation est en cours. |
REVALIDATED | L'upstream a confirmé que la version en cache est encore valide (304). |
Bypass pour les utilisateurs authentifiés
# Ne pas cacher si l'utilisateur est connecté (cookie de session présent) proxy_cache_bypass $cookie_session_id; proxy_no_cache $cookie_session_id; # Ou bypass si X-No-Cache envoyé par l'upstream proxy_cache_bypass $http_x_no_cache;
proxy_cache_lock on;, une seule requête part à l'upstream — les 99 autres attendent le résultat. Indispensable sur du trafic élevé.
Purge du cache Nginx
Nginx ne supporte pas la purge par URL en version libre. Options :
- Module
ngx_cache_purge(tiers) : ajoute une méthode PURGE HTTP. - Supprimer les fichiers sur disque directement (pas recommandé en production).
- TTL court +
stale-while-revalidatepour éviter d'avoir à purger. - Passer à une solution avec purge native (Varnish, CDN cloud).
Synthèse — choisir les bons headers
Pour chaque type de ressource (HTML, CSS/JS, API, images) — quel Cache-Control, quel TTL, quelle stratégie d'invalidation.
Tous les concepts sont posés. Il est temps de les assembler en règles pratiques. La question centrale est toujours la même : pour ce type de ressource, quel est le bon équilibre entre fraîcheur et performance ?
Guide par type de ressource
| Ressource | Cache-Control recommandé | Stratégie d'invalidation |
|---|---|---|
| Assets CSS/JS avec hash | public, max-age=31536000, immutable |
Versioning URL — hash change = nouvelle URL |
| Images avec hash | public, max-age=31536000, immutable |
Versioning URL — CDN cache permanent |
| Page HTML statique (blog) | public, max-age=60, s-maxage=86400, stale-while-revalidate=3600 |
TTL court browser + purge CDN à la publication |
| Page HTML dynamique (e-commerce) | public, max-age=0, s-maxage=300, stale-while-revalidate=60 |
Cache tags + purge event-driven |
| API JSON publique (catalogue) | public, max-age=60, s-maxage=3600, stale-while-revalidate=300 |
ETag + TTL |
| API JSON personnalisée (profil) | private, max-age=300, must-revalidate |
Browser uniquement — ETag |
| Données temps réel (prix, stock) | public, max-age=10, must-revalidate |
TTL très court — purge à chaque changement |
| Données sensibles (panier, paiement) | no-store |
Aucun cache |
| Fonts Google Fonts (via CDN externe) | (géré par Google CDN) | immutable de facto — URL versionnée |
Checklist de mise en production
- Assets CSS/JS : hash dans le nom de fichier ?
immutableposé ? - Pages HTML :
s-maxagedifférent demax-agepour le CDN ? - APIs privées :
privatepour bloquer les caches partagés ? - Données critiques :
must-revalidateouno-store? - Vary: Accept-Encoding présent sur les réponses compressées ?
- X-Cache-Status visible pour le debugging ?
- Stratégie d'invalidation documentée pour chaque type de ressource ?
Les erreurs les plus courantes
no-cache = stocké mais revalidé. no-store = jamais stocké.
max-age=60 signifie que ton CDN ne garde rien plus d'une minute. Chaque requête atteint l'origin. Utilise s-maxage pour découpler la durée browser de la durée CDN.
Architecture de référence
L1 Browser
L3 s-maxage
L4 origin protection
L2 proxy_cache
L5 app cache
source de vérité
Chaque couche a sa responsabilité. Le CDN absorbe le pic de trafic mondial. Le reverse proxy Nginx protège les serveurs applicatifs. Redis évite les requêtes SQL répétitives. Et les headers HTTP orchestrent tout ça de façon déclarative — tu décris ce que tu veux, les caches décident comment le faire.