Un site e-commerce de 22 000 pages produit, TTFB moyen de 1,8 seconde, un Googlebot qui abandonne le crawl après 3 000 URLs par session. Passage à un stack Varnish + Redis + Cloudflare : TTFB ramené à 85 ms, 18 000 URLs crawlées par session. Même contenu, même serveur, même budget — juste une stratégie de cache cohérente.
Le cache serveur n'est pas un sujet de performance pure. C'est un levier direct sur le crawl budget et, par extension, sur la vitesse d'indexation et la couverture de votre catalogue.
Pourquoi le cache serveur impacte directement le crawl budget
Googlebot fonctionne avec une enveloppe de crawl par site, ajustée dynamiquement selon deux facteurs : la capacité du serveur à répondre vite (crawl rate limit) et la valeur perçue des pages à crawler (crawl demand). Vous ne contrôlez pas le second. Vous contrôlez totalement le premier.
Le crawl rate limit en pratique
Quand Googlebot détecte que votre serveur ralentit — TTFB qui passe au-dessus de 500 ms, taux d'erreurs 5xx qui monte — il réduit automatiquement la fréquence de ses requêtes. La documentation Google Search Central le confirme explicitement : le crawl rate est calculé pour ne pas dégrader l'expérience utilisateur.
Chaque milliseconde de TTFB gagné permet à Googlebot de faire plus de requêtes dans la même fenêtre. Sur un site de 15 000+ pages, la différence entre un TTFB de 1,2 s et un TTFB de 100 ms, c'est potentiellement un facteur 10 sur le nombre de pages crawlées par jour.
L'analyse de vos logs serveur permet de vérifier ce phénomène directement. Si vous ne faites pas déjà de log analysis pour le SEO, vous pilotez à l'aveugle. Identifiez les plages horaires où Googlebot crawle, corrélez avec votre TTFB moyen sur ces plages, et vous verrez la relation linéaire.
Cache miss vs cache hit : l'impact sur le rendering pipeline
Un point souvent négligé : quand Googlebot crawle une page qui nécessite du server-side rendering (Next.js, Nuxt, etc.), un cache miss signifie un rendu complet côté serveur. Sur un site React avec SSR, un rendu peut prendre 200 à 800 ms selon la complexité des composants et les appels API internes. Multipliez ça par 5 000 pages crawlées et vous consommez votre crawl rate pour rien.
Un cache hit Varnish renvoie la réponse en 2-15 ms. Googlebot reçoit le HTML complet sans attendre le rendering. C'est la stratégie la plus efficace pour les sites qui dépendent du SSR pour leur SEO.
Varnish : le reverse proxy cache pour le crawl
Varnish Cache s'installe devant votre serveur applicatif et sert les réponses HTTP depuis la RAM. Pour le SEO, c'est le composant le plus impactant de la stack car il élimine complètement le temps de traitement applicatif pour les pages déjà en cache.
Configuration VCL orientée SEO
Le piège classique : une configuration Varnish qui cache les pages pour les utilisateurs mais pas pour Googlebot, parce que le VCL (Varnish Configuration Language) fait un bypass sur certains headers.
Voici une configuration VCL de base qui priorise le cache pour les crawlers tout en gérant correctement les cas SEO critiques :
vcl 4.1;
backend default {
.host = "127.0.0.1";
.port = "8080";
.connect_timeout = 5s;
.first_byte_timeout = 15s;
.between_bytes_timeout = 5s;
}
sub vcl_recv {
# Supprimer les cookies pour les pages publiques (produits, catégories, articles)
# Les cookies empêchent le cache et sont inutiles pour Googlebot
if (req.url ~ "^/(products|categories|blog|pages)/") {
unset req.http.Cookie;
}
# Ne JAMAIS cacher les pages avec paramètres de session
if (req.url ~ "[?&](sid|session|token)=") {
return (pass);
}
# Bypass cache pour les pages de checkout/compte
if (req.url ~ "^/(checkout|account|cart|api)/") {
return (pass);
}
# Normaliser le header Host pour éviter les duplications de cache
set req.http.Host = regsub(req.http.Host, ":[0-9]+", "");
# Normaliser les query strings : trier les paramètres pour maximiser le hit rate
# (nécessite le vmod querystring)
# set req.url = querystring.sort(req.url);
return (hash);
}
sub vcl_backend_response {
# TTL par défaut : 1 heure pour les pages produit
if (bereq.url ~ "^/products/") {
set beresp.ttl = 1h;
set beresp.grace = 24h; # Servir le stale pendant 24h si le backend est down
}
# Pages catégories : cache plus court car changements fréquents
if (bereq.url ~ "^/categories/") {
set beresp.ttl = 15m;
set beresp.grace = 6h;
}
# Pages blog/contenu : cache long
if (bereq.url ~ "^/blog/") {
set beresp.ttl = 6h;
set beresp.grace = 48h;
}
# Ne pas cacher les réponses avec Set-Cookie
if (beresp.http.Set-Cookie) {
set beresp.uncacheable = true;
return (deliver);
}
# Ne pas cacher les erreurs 5xx
if (beresp.status >= 500) {
set beresp.uncacheable = true;
set beresp.ttl = 0s;
return (deliver);
}
return (deliver);
}
sub vcl_deliver {
# Header de debug pour identifier cache HIT/MISS dans les logs
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT";
set resp.http.X-Cache-Hits = obj.hits;
} else {
set resp.http.X-Cache = "MISS";
}
}
Les points critiques pour le SEO
Le paramètre grace est essentiel. Quand le TTL expire et que le backend est lent ou down, Varnish sert la version stale pendant la durée du grace. Sans ça, une panne backend de 30 secondes pendant un crawl intensif de Googlebot génère des centaines de 5xx — et un crawl rate qui s'effondre pour des jours.
Attention aux redirections cachées. Si votre backend renvoie un 301 et que Varnish le cache, vérifiez que le TTL est adapté. Cacher un 301 temporaire (qui devrait être un 302) pendant 6 heures peut bloquer des corrections. Référez-vous au guide complet des status codes HTTP pour les implications SEO de chaque code.
Ne cachez jamais les pages avec un rel=canonical dynamique sans vérifier que le canonical ne varie pas selon l'utilisateur. Sur certaines plateformes e-commerce, le canonical inclut des paramètres de tracking — la version cachée pourrait servir un canonical incorrect à Googlebot.
Monitorer le hit rate Varnish
# Voir le hit rate en temps réel
varnishstat -f MAIN.cache_hit -f MAIN.cache_miss
# Analyser les requêtes Googlebot spécifiquement
varnishlog -q 'ReqHeader:User-Agent ~ "Googlebot"' -i ReqURL,RespStatus,RespHeader:X-Cache
# Exporter les stats pour analyse
varnishncsa -F '%{Host}i %U %{Varnish:handling}x %D %s' -q 'ReqHeader:User-Agent ~ "Googlebot"' > /var/log/varnish/googlebot_cache.log
La commande varnishlog filtrée sur Googlebot est un outil de diagnostic puissant. Vous verrez immédiatement quelles URLs génèrent des cache miss pour le crawler. Un hit rate inférieur à 80% pour Googlebot signifie que votre VCL a un problème — probablement des cookies ou des headers Vary trop larges.
Redis : le cache applicatif pour les pages dynamiques
Varnish gère le cache HTTP complet. Redis intervient en amont, au niveau applicatif, pour cacher les résultats de requêtes database, les fragments de page, ou le HTML pré-rendu.
Quand Redis est plus pertinent que Varnish
Varnish excelle pour les pages dont le HTML complet peut être servi tel quel. Redis est plus adapté quand :
- La page contient des éléments personnalisés (stock en temps réel, prix par géolocalisation) qui doivent être assemblés dynamiquement
- Vous avez besoin d'un cache granulaire au niveau des fragments (le bloc "produits similaires" qui est identique sur 500 pages)
- Votre framework applicatif gère son propre cache interne (Laravel, Django, Rails)
Sur un site e-commerce avec Product Schema, les données structurées contiennent des prix et disponibilités qui changent fréquemment. Cacher le HTML complet en Varnish avec un TTL d'1 heure risque de servir des données structurées obsolètes. Cacher le fragment de données produit en Redis avec un TTL de 5 minutes, et le template HTML en Varnish avec un TTL d'1 heure est bien plus adapté.
Pattern de cache Redis pour le SSR SEO
Voici un pattern concret pour un site Next.js qui utilise Redis comme cache de rendu SSR :
// lib/cache.ts
import Redis from 'ioredis';
import { createHash } from 'crypto';
const redis = new Redis({
host: process.env.REDIS_HOST || '127.0.0.1',
port: 6379,
maxRetriesPerRequest: 1,
lazyConnect: true,
});
interface CacheOptions {
ttl: number; // TTL en secondes
staleTtl?: number; // Durée pendant laquelle on sert du stale (stale-while-revalidate)
tags?: string[]; // Tags pour invalidation groupée
}
export async function getCachedSSR(
url: string,
renderFn: () => Promise<string>,
options: CacheOptions
): Promise<{ html: string; fromCache: boolean }> {
const cacheKey = `ssr:${createHash('md5').update(url).digest('hex')}`;
try {
const cached = await redis.get(cacheKey);
if (cached) {
const { html, timestamp } = JSON.parse(cached);
const age = Date.now() - timestamp;
const isStale = age > options.ttl * 1000;
if (isStale && options.staleTtl && age < (options.ttl + options.staleTtl) * 1000) {
// Stale-while-revalidate : servir le stale et régénérer en arrière-plan
setImmediate(async () => {
try {
const freshHtml = await renderFn();
await storeInCache(cacheKey, freshHtml, options);
} catch (e) {
console.error(`Background revalidation failed for ${url}`, e);
}
});
return { html, fromCache: true };
}
if (!isStale) {
return { html, fromCache: true };
}
}
// Cache miss ou stale expiré
const html = await renderFn();
await storeInCache(cacheKey, html, options);
return { html, fromCache: false };
} catch (error) {
// Redis down : fallback direct sur le render sans cache
console.error('Redis cache error, falling back to direct render', error);
const html = await renderFn();
return { html, fromCache: false };
}
}
async function storeInCache(key: string, html: string, options: CacheOptions) {
const data = JSON.stringify({ html, timestamp: Date.now() });
const totalTtl = options.ttl + (options.staleTtl || 0);
await redis.setex(key, totalTtl, data);
// Stocker les tags pour invalidation groupée
if (options.tags) {
const pipeline = redis.pipeline();
for (const tag of options.tags) {
pipeline.sadd(`tag:${tag}`, key);
pipeline.expire(`tag:${tag}`, totalTtl);
}
await pipeline.exec();
}
}
// Invalidation par tag : quand un produit change, invalider toutes les pages qui l'affichent
export async function invalidateByTag(tag: string): Promise<number> {
const keys = await redis.smembers(`tag:${tag}`);
if (keys.length === 0) return 0;
const pipeline = redis.pipeline();
for (const key of keys) {
pipeline.del(key);
}
pipeline.del(`tag:${tag}`);
await pipeline.exec();
return keys.length;
}
L'implémentation du stale-while-revalidate au niveau applicatif est le détail qui fait la différence. Googlebot reçoit toujours une réponse rapide (la version stale), pendant que le serveur régénère la page en arrière-plan. Résultat : zéro cache miss visible pour le crawler.
Invalidation et SEO : le piège du cache trop long
L'invalidation par tags résout un problème concret : quand vous mettez à jour le prix d'un produit, vous devez invalider non seulement la fiche produit, mais aussi toutes les pages catégories qui affichent ce produit, les pages de recherche interne qui le listent, et potentiellement la homepage si le produit y est mis en avant.
Sans invalidation granulaire, vous êtes coincé entre deux mauvais choix : un TTL court (5 minutes) qui génère beaucoup de cache miss et annule le bénéfice, ou un TTL long (6 heures) qui sert des données obsolètes — un vrai problème quand Google durcit les règles sur les pages produit en rupture.
CDN : la couche de cache en edge pour le TTFB mondial
Le CDN ajoute une couche de cache géographiquement distribuée. Pour le SEO, l'intérêt principal est double : réduire le TTFB pour les crawlers (Googlebot crawle majoritairement depuis les US) et absorber les pics de crawl sans surcharger votre origin.
Configuration Cloudflare orientée SEO
Si vous utilisez Cloudflare — et la configuration correcte est essentielle pour ne pas casser le SEO — voici les cache rules à mettre en place pour maximiser le hit rate crawler :
# Cloudflare Page Rules (ou Cache Rules dans le nouveau dashboard)
# 1. Pages produit : cache edge 2h, browser cache 0 (pas de cache navigateur pour les prix dynamiques)
URL pattern: shop.example.com/products/*
Cache Level: Cache Everything
Edge Cache TTL: 7200
Browser Cache TTL: 0
Origin Cache Control: Off
# 2. Pages catégories : cache edge 30 min
URL pattern: shop.example.com/categories/*
Cache Level: Cache Everything
Edge Cache TTL: 1800
Browser Cache TTL: 0
# 3. Assets statiques : cache agressif
URL pattern: shop.example.com/assets/*
Cache Level: Cache Everything
Edge Cache TTL: 2592000
Browser Cache TTL: 2592000
# 4. API / checkout : bypass total
URL pattern: shop.example.com/api/*
Cache Level: Bypass
# 5. Sitemap : cache court pour que les mises à jour soient visibles rapidement
URL pattern: shop.example.com/sitemap*.xml
Cache Level: Cache Everything
Edge Cache TTL: 600
Le header Vary et ses pièges avec le CDN
Un problème fréquent sur les sites qui servent du contenu différent selon le device (HTML distinct pour mobile et desktop, pas du responsive) : le header Vary: User-Agent fait exploser le nombre de variantes en cache. Cloudflare et la plupart des CDN ne cachent pas les réponses avec Vary: User-Agent car il existe des milliers de User-Agent différents.
La solution : utiliser Vary: Accept-Encoding (standard) et gérer la variation mobile/desktop via un header custom comme X-Device-Type que vous settez dans un Cloudflare Worker :
// Cloudflare Worker : normaliser le device type pour le cache
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const ua = request.headers.get('User-Agent') || '';
// Détecter le type de device de manière grossière mais suffisante
const isMobile = /Mobile|Android|iPhone|iPad/i.test(ua);
// Googlebot mobile et desktop doivent recevoir la bonne version
const isGooglebotMobile = /Googlebot.*Mobile/i.test(ua);
const isGooglebotDesktop = /Googlebot/i.test(ua) && !isGooglebotMobile;
let deviceType = 'desktop';
if (isMobile || isGooglebotMobile) {
deviceType = 'mobile';
}
// Créer une nouvelle requête avec le header custom
const modifiedRequest = new Request(request, {
headers: new Headers(request.headers),
});
modifiedRequest.headers.set('X-Device-Type', deviceType);
// Le cache key inclut maintenant le device type au lieu du User-Agent complet
const cacheKey = `${request.url}::${deviceType}`;
const cache = caches.default;
let response = await cache.match(cacheKey);
if (!response) {
response = await fetch(modifiedRequest);
// Ne cacher que les réponses 200
if (response.status === 200) {
const responseToCache = response.clone();
// Modifier le Vary pour utiliser notre header custom
const headers = new Headers(responseToCache.headers);
headers.set('Vary', 'X-Device-Type, Accept-Encoding');
const cachedResponse = new Response(responseToCache.body, {
status: responseToCache.status,
headers,
});
event.waitUntil(cache.put(cacheKey, cachedResponse));
}
}
return response;
}
Ce pattern est particulièrement pertinent pour les sites qui ont encore un setup m.example.com ou du dynamic serving. Si vous êtes en responsive design pur, vous n'avez pas ce problème — mais vérifiez que votre origin ne renvoie pas un Vary: User-Agent par accident (certains CMS le font par défaut).
Scénario concret : migration de cache sur un catalogue e-commerce de 18 000 pages
Un retailer mode français, 18 000 fiches produit, 450 pages catégories, site sur Magento 2 avec un TTFB moyen de 2,1 secondes. Le crawl Google Search Console montrait 1 200 pages crawlées par jour en moyenne — insuffisant pour un catalogue qui bouge quotidiennement (nouveaux produits, changements de prix, ruptures de stock).
Avant : le diagnostic
En analysant les logs serveur (extraction via GoAccess filtrée sur les User-Agent Googlebot), le constat était clair :
- TTFB moyen Googlebot : 2 340 ms (plus élevé que le TTFB utilisateur car Googlebot ne bénéficie pas du cache navigateur)
- 23% des requêtes Googlebot renvoyaient un 5xx (timeouts PHP-FPM sous charge)
- Googlebot se concentrait sur les pages catégories et ignorait 60% des fiches produit
- Temps moyen entre la mise en ligne d'un produit et son premier crawl : 11 jours
La stack mise en place
-
Redis (cache applicatif) : cache des blocs Magento (navigation, widgets, blocs CMS) + full page cache Redis via le module natif Magento 2. TTL de 3600s pour les fiches produit, 900s pour les catégories. Invalidation événementielle via les observers Magento (catalog_product_save_after, etc.).
-
Varnish 7 (reverse proxy) : devant Nginx, avec la VCL officielle Magento 2 adaptée. Grace period de 6 heures. Health check toutes les 5 secondes sur le backend.
-
Cloudflare (CDN) : cache rules par type de page. Tiered caching activé pour réduire les requêtes vers l'origin. Cache Reserve activé pour les pages à faible trafic (longue traîne) qui seraient normalement évictées rapidement du cache edge.
Après : les résultats à 6 semaines
- TTFB moyen Googlebot : 67 ms (cache hit Varnish) / 380 ms (cache miss Varnish, hit Redis)
- Taux d'erreur 5xx Googlebot : 0,1% (uniquement pendant les déploiements)
- Pages crawlées par jour : 5 800 (x4,8)
- Temps moyen entre mise en ligne et premier crawl : 2,3 jours
- Couverture d'indexation passée de 71% à 94% du catalogue en 6 semaines
- Trafic organique sur les fiches produit : +34% à 8 semaines (corrélation directe avec l'augmentation de couverture d'indexation)
Le gain de trafic n'est pas lié à un meilleur classement des pages déjà indexées. Il vient du fait que 4 100 fiches produit qui n'étaient pas indexées le sont devenues — simplement parce que Googlebot pouvait enfin les atteindre dans son budget de crawl.
Stale-while-revalidate et les headers de cache HTTP pour Googlebot
Un aspect souvent mal compris : comment Googlebot interprète les directives de cache HTTP. Gary Illyes de Google a confirmé à plusieurs reprises que Googlebot respecte les headers Cache-Control de manière sélective — il ne cache pas les pages comme un navigateur, mais il utilise les directives comme signal.
Les headers qui comptent pour le crawl
Le header stale-while-revalidate dans Cache-Control indique aux caches intermédiaires qu'ils peuvent servir une version expirée pendant qu'ils revalident en arrière-plan. C'est exactement ce que vous voulez pour Googlebot : une réponse instantanée, toujours.
Cache-Control: public, max-age=3600, stale-while-revalidate=86400, stale-if-error=86400
Cette directive dit : cache valide 1 heure, mais servir la version stale jusqu'à 24h tout en revalidant, et servir la version stale 24h en cas d'erreur backend. Varnish, Cloudflare, et Fastly supportent tous cette directive.
Le stale-if-error est votre filet de sécurité. Si votre backend crash pendant un crawl intensif de Googlebot, les caches intermédiaires continuent à servir des réponses 200 au lieu de propager des 5xx. C'est la différence entre un incident invisible pour le SEO et une chute de crawl rate qui met des semaines à se rétablir.
Ce qu'il ne faut PAS faire
Ne renvoyez jamais Cache-Control: no-store sur vos pages SEO. Cette directive empêche tout cache intermédiaire de stocker la réponse — elle tue Varnish, le CDN, et tout le bénéfice de votre stack. Si vous avez besoin d'empêcher le cache navigateur (pour des raisons de contenu dynamique), utilisez Cache-Control: public, max-age=0, s-maxage=3600 — ça autorise le cache serveur (s-maxage) tout en empêchant le cache navigateur (max-age=0).
Diagnostiquer les problèmes de cache avec les bons outils
Vérifier le comportement de cache depuis l'extérieur
# Vérifier les headers de cache pour une URL spécifique
curl -sI -H "User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" \
https://shop.example.com/products/veste-en-lin-bleue | grep -iE "cache-control|x-cache|age|cf-cache|vary"
# Résultat attendu :
# Cache-Control: public, max-age=3600, stale-while-revalidate=86400
# X-Cache: HIT
# Age: 1247
# CF-Cache-Status: HIT
# Vary: Accept-Encoding
Si X-Cache dit MISS et CF-Cache-Status dit DYNAMIC, votre page n'est cachée nulle part. Cherchez un Set-Cookie parasite, un Vary: User-Agent, ou un Cache-Control: private renvoyé par votre backend.
Screaming Frog pour auditer le cache à l'échelle
Configurez Screaming Frog pour extraire les headers de cache custom dans l'onglet Configuration > Custom > Extraction > HTTP Headers. Extrayez X-Cache, CF-Cache-Status, Age, et Cache-Control. Crawlez votre site complet, exportez en CSV, et calculez votre hit rate par template de page. Un hit rate global sous 70% signifie que des pans entiers de votre site ne sont pas cachés.
Search Console et les signaux indirects
Google Search Console ne montre pas directement l'impact du cache. Mais le rapport "Statistiques d'exploration" (Settings > Crawl Stats) donne le temps de réponse moyen et le nombre de requêtes par jour. Après la mise en place du cache, surveillez la courbe du temps de réponse moyen — une chute brutale confirmera que le cache fonctionne pour Googlebot. Et si le nombre de requêtes par jour augmente dans les jours qui suivent, vous avez votre preuve.
Google a par ailleurs confirmé déployer des centaines de crawlers non documentés. Votre stack de cache doit fonctionner pour tous les User-Agent, pas seulement pour "Googlebot". Ne faites jamais de logique conditionnelle basée sur le User-Agent dans votre VCL — cachez uniformément.
La stack complète : comment les trois couches s'articulent
L'erreur commune est de voir Varnish, Redis et le CDN comme des alternatives. Ce sont des couches complémentaires qui interviennent à des niveaux différents :
- CDN (Cloudflare/Fastly) → cache en edge, proche géographiquement de Googlebot. Première couche touchée. Gère les assets statiques et les pages à faible variabilité.
- Varnish → cache HTTP au niveau de l'origin. Intercepte les requêtes qui passent le CDN (cache miss edge, contenu dynamique). Gère le grace/stale-if-error.
- Redis → cache applicatif. Accélère la génération des pages quand Varnish a un cache miss. Cache les fragments, les résultats de requêtes DB, le HTML pré-rendu.
Le flux optimal pour une requête Googlebot : CDN HIT → réponse en 15 ms. CDN MISS → Varnish HIT → réponse en 50 ms. Varnish MISS → Redis HIT sur les fragments → page assemblée en 150 ms. Triple MISS complet → rendu full serveur en 800 ms+ (mais ce cas ne devrait concerner que les nouvelles pages jamais crawlées).
Pour un site qui dépend de JavaScript pour son rendu, cette stack élimine le problème à la racine : le HTML servi à Googlebot est toujours pré-rendu et caché, pas généré à la volée côté client.
La mise en place d'une stratégie de cache serveur cohérente est probablement le levier technique qui offre le meilleur ratio effort/impact pour le crawl budget. Mais le cache introduit aussi de la complexité — une invalidation ratée, un header mal configuré, et vous servez du contenu obsolète à Googlebot pendant des heures sans le savoir. Un outil de monitoring comme Seogard permet de détecter ces régressions en continu : une meta description qui disparaît parce que le cache sert une ancienne version, un canonical qui change entre cache hit et cache miss, un TTFB qui explose sur un template spécifique. Le cache accélère le crawl. Le monitoring s'assure qu'il ne casse rien.