← Tous les cours / Reverse Proxy, CDN & Cache Cours
00 — LE REVERSE PROXY

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.

Reverse proxy
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 :

FonctionCe 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 :

Forward proxy

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.

Reverse proxy

Côté serveur. Le client ne sait pas qu'il existe. Il cache pour le serveur — soulage l'origin des requêtes identiques.

Client
Reverse Proxy
(Nginx, HAProxy, Caddy)
Upstream A
Upstream B

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
💡 Connection keep-alive : le reverse proxy maintient un pool de connexions TCP persistantes vers les upstreams. Créer une nouvelle connexion TCP à chaque requête coûterait inutilement des millisecondes. Avec keep-alive, la connexion reste ouverte et les requêtes s'enchaînent dessus.

Simulateur — Routing reverse proxy
Clique sur "Envoyer une requête" pour voir le proxy distribuer le trafic entre ses deux upstreams en round-robin.

01 — CACHE-CONTROL

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.

Cache-Control
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

DirectiveQui ça concerneSignification
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 :

💡 Rappel : 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 ≠ no-store : 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.

02 — ETAG & LAST-MODIFIED

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.

Revalidation conditionnelle
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

TypeSyntaxeGarantie
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.
💡 Préfère les ETags forts sauf si tu ne peux pas garantir des octets identiques pour un même contenu (ex: contenu généré dynamiquement avec horodatage). Les ETags faibles ne peuvent pas être utilisés avec 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
🔑 À retenir : ETag + If-None-Match = économie de bande passante (cache). ETag + If-Match = protection contre les conflits d'édition concurrente.
03 — VARY

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.

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 :

  1. 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.
  2. 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.
⚠️ Vary: Cookie est à éviter absolument sur un CDN. Chaque utilisateur a un cookie unique → chaque réponse a sa propre entrée en cache → le cache ne sert à rien et l'origin est submergé. Si une ressource varie selon l'utilisateur, utilise plutôt 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
🔑 À retenir : chaque valeur unique du header listé dans Vary crée une entrée de cache séparée. Plus tu varies sur des headers très variables (User-Agent entier, Cookie…), plus ton cache se fragmente et perd en efficacité.
04 — CDN

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.

CDN — Content Delivery Network
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+.

Utilisateur Paris
PoP Paris
(CDN Edge)
→ (si MISS)
Ton origin

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

Origin pull (lazy)

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.

Origin push (eager)

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 :

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.

PoP Sydney (MISS)
Shield London
(mid-tier)
Origin
PoP Singapour (MISS)
💡 Activer le shield est la première chose à faire quand ton origin est sous pression après un déploiement ou une invalidation de cache globale. Réduit les requêtes origin de 90% ou plus sur du contenu populaire.
05 — COUCHES DE CACHE

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

#CouchePortéeProtocoleTTL 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
Browser
CDN Edge
CDN Shield
Reverse Proxy
App + Redis
Base de données

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 :

  1. Browser : MISS → envoie la requête vers le CDN Edge.
  2. CDN Edge : MISS → contacte le CDN Shield.
  3. CDN Shield : MISS → contacte le reverse proxy.
  4. Reverse proxy : MISS → contacte l'application PHP.
  5. Application PHP : OPcache HIT, mais Redis MISS → contacte la BDD.
  6. BDD : requête SQL → résultat retourné.
  7. Chaque couche stocke la réponse au retour → les prochaines requêtes sont HIT.
💡 Cache warming : après un déploiement, il est courant de "préchauffer" le cache en simulant des requêtes sur les URLs principales avant d'activer le nouveau code en production. Cela évite le "thundering herd" — des centaines de requêtes simultanées qui frappent l'origin à froid.

L1–L4 vs L5 — deux paradigmes différents

L1–L4 : Cache HTTP

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.

L5 : Application cache (Redis)

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).

🔑 À retenir : les couches L1–L4 te coûtent presque zéro à implémenter — il suffit de mettre les bons headers HTTP. La couche L5 (Redis) est plus complexe à invalider correctement. Commence toujours par optimiser les couches HTTP avant d'ajouter du Redis.
06 — INVALIDATION

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.

Invalidation de cache
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.

Rédacteur publie
App webhook
API CDN purge
Cache purgé

Simulateur — Stratégies d'invalidation
Sélectionne un type de ressource pour voir la stratégie d'invalidation adaptée, puis simule un changement.

07 — STALE-WHILE-REVALIDATE

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.

stale-while-revalidate=N
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

Sans (comportement par défaut)
  1. Ressource expire.
  2. Visiteur A arrive → cache bloqué → attend le serveur → reçoit la réponse fraîche.
  3. Visiteur A attend 200–500 ms de latence origin.
Avec stale-while-revalidate
  1. Ressource expire.
  2. Visiteur A arrive → cache sert immédiatement la version stale.
  3. En parallèle, le cache contacte le serveur en background.
  4. 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
💡 CDN vs browser : 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é.
08 — NGINX CACHE

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

ValeurSignification
HITRéponse servie depuis le cache Nginx. L'upstream n'a pas été contacté.
MISSPas en cache. L'upstream a été contacté, la réponse est maintenant stockée.
EXPIREDEn cache mais TTL expiré. L'upstream a été contacté pour revalider.
BYPASSCache ignoré (ex: requête avec Cookie, ou proxy_cache_bypass actif).
STALEVersion expirée servie car proxy_cache_use_stale était actif et l'upstream en erreur.
UPDATINGVersion stale servie pendant qu'une revalidation est en cours.
REVALIDATEDL'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;
💡 Cache lock (proxy_cache_lock) : par défaut, si 100 requêtes arrivent simultanément pour une URL en MISS, Nginx envoie 100 requêtes à l'upstream. Avec 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 :

09 — SYNTHÈSE

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

RessourceCache-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

Avant de déployer
  • Assets CSS/JS : hash dans le nom de fichier ? immutable posé ?
  • Pages HTML : s-maxage différent de max-age pour le CDN ?
  • APIs privées : private pour bloquer les caches partagés ?
  • Données critiques : must-revalidate ou no-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 vs no-store : confondre les deux est la faute #1. no-cache = stocké mais revalidé. no-store = jamais stocké.
⚠️ Oublier s-maxage : mettre seulement 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.
⚠️ Vary: Cookie sur le CDN : chaque cookie unique fragmente le cache. Une page "publique" avec une variante par cookie = taux de HIT proche de zéro. Strip les cookies non pertinents dans la configuration CDN.
💡 Mesure ton hit rate : un bon cache CDN doit avoir un hit rate ≥ 80-90% sur le contenu statique. Si tu es en dessous, cherche d'abord les problèmes de cache key (cookies, query params) avant d'augmenter les TTLs.

Architecture de référence

Client
L1 Browser
CDN Edge
L3 s-maxage
CDN Shield
L4 origin protection
Nginx
L2 proxy_cache
PHP + Redis
L5 app cache
PostgreSQL
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.