layercache – bibliothèque de cache multicouche pour Node.js
(github.com/flyingsquirrel0419)Qu’est-ce que layercache ?
Il s’agit d’une bibliothèque de cache multicouche pour Node.js qui regroupe Memory → Redis → Disk derrière une API unique.
En cas de hit, la valeur est récupérée depuis la couche la plus rapide et les couches supérieures sont remplies automatiquement. En cas de miss, même si 100 requêtes concurrentes arrivent, le fetcher n’est exécuté qu’une seule fois.
Pourquoi l’avoir créé ?
Quand on exploite un service Node.js, la manière d’empiler les couches de cache suit généralement un schéma assez similaire. On commence par un cache en mémoire, puis quand le nombre d’instances augmente, on ajoute Redis, puis on finit par rencontrer des problèmes de stampede, des incohérences de cache entre instances, etc. Chaque problème peut être résolu, mais tout assembler d’un coup à un niveau production demandait plus de travail que prévu.
Je l’ai donc créé avec l’idée de faire ce travail correctement une bonne fois pour toutes.
Quelles sont les principales fonctionnalités ?
Fonctionnement clé
- Lecture en couches + backfill automatique (miss L1 → lecture L2 → remplissage de L1)
- Stampede prevention : 100 requêtes concurrentes → 1 seule exécution du
fetcher - Distributed single-flight : suppression des exécutions en double entre instances grâce au verrou distribué Redis
- Bus d’invalidation L1 basé sur Redis pub/sub (synchronisation du cache mémoire entre instances)
Invalidation / fraîcheur
- Invalidation basée sur des tags, invalidation par wildcard/préfixe
- Stale-while-revalidate, Stale-if-error
- Sliding TTL, Adaptive TTL, Refresh-ahead
Résilience
- Graceful degradation : en cas de panne Redis, saut de la couche concernée puis reprise automatique
- Circuit breaker
- Politiques d’écriture strict / best-effort
Observabilité
- Exporter Prometheus, hooks OpenTelemetry
- Mesure de la latence par couche, hooks d’événements
- CLI d’administration (
npx layercache stats|keys|invalidate)
Intégration framework
Express, Fastify, Hono, tRPC, GraphQL, Next.js
Vous voulez connaître les chiffres du benchmark ?
Ils ont été mesurés sur une VM monocœur avec un Redis Docker réel.
| Scénario | Latence moyenne |
| L1 mémoire, warm hit | 0.005 ms |
| L2 Redis, warm hit (1 KiB) | 0.193 ms |
| Sans cache (simulation DB) | 5.030 ms |
- Débit HTTP :
/layered16,211 req/s vs/nocache158 req/s - Stampede : 75 requêtes concurrentes → 5 fetchs originaux (contre 375 sans cache)
- Distributed single-flight : 60 requêtes concurrentes → 1 fetch original
La méthodologie complète du benchmark et les résultats bruts sont détaillés dans docs/benchmarking.md.
Qu’est-ce qui le différencie des bibliothèques existantes ?
node-cache-manager, keyv et cacheable sont tous de bons choix. Pour résumer brièvement les différences :
- Stampede prevention / Distributed single-flight : aucune de ces trois bibliothèques ne les fournit par défaut. layercache a été conçu autour de ces deux points.
- Cross-instance L1 invalidation : le cache mémoire est synchronisé automatiquement entre instances via Redis pub/sub. Cela permet d’utiliser sereinement un cache mémoire dans un environnement multi-instance.
- Auto backfill : les couches supérieures sont remplies automatiquement lorsqu’une couche inférieure répond.
- Graceful degradation + Circuit breaker : même si Redis tombe, le service continue de fonctionner.
Installation et liens
npm install layercache
- GitHub : https://github.com/flyingsquirrel0419/layercache
- npm : https://www.npmjs.com/package/layercache
Si vous avez des questions sur les choix de conception, en particulier sur la coordination du single-flight ou le fonctionnement du graceful degradation, n’hésitez pas à les poser.
4 commentaires
Bonne bibliothèque !
Y a-t-il une raison pour laquelle Redis a été inclus dans la conception ? Est-ce que vous partez du principe que plusieurs instances en lecture seule démarrent en même temps ? Dans ce cas, le disque (local) ne devrait-il pas être placé dans une couche en amont de Redis ?
Redis est inclus en partant du principe qu’il y a plusieurs serveurs. Comme la mémoire de chaque serveur peut contenir des valeurs différentes, Redis joue le rôle de « source de vérité » partagée.
Si le disque vient après Redis, c’est parce qu’en supposant que Redis se trouve sur le même réseau local, il est plus rapide. D’après les benchmarks, le disque est à ~2 ms, tandis que Redis est à ~0,02 ms. Mais si Redis est éloigné ou que le réseau est mauvais, le disque local peut être plus rapide ; dans ce cas, il est logique d’inverser l’ordre. La bibliothèque n’impose d’ailleurs pas cet ordre : c’est l’utilisateur qui le définit.
Quel que soit son emplacement, le rôle principal du disque n’est pas d’être le plus rapide, mais de servir de dernier filet de sécurité quand Memory et Redis sont tous les deux hors service.
Merci pour votre intention de conception.
Si je comprends bien, vous enregistrez tous les appels distants en écriture sur le disque local, puis vous relisez sur le disque lorsque l’appel distant échoue, c’est bien cela ? Il pourrait aussi être utile de se demander si une couche Disk est vraiment indispensable dans la couche de cache.
DiskLayer ne suit pas ce type de pattern et fonctionne simplement comme une couche de cache classique — elle lit et écrit, et lorsqu’il y a un miss sur la couche supérieure, l’accès se fait dans l’ordre des couches. C’était source de confusion de ma part.
Le pattern que vous mentionnez, consistant à « enregistrer le résultat d’un appel distant sur disque puis le relire en cas d’échec », se rapproche en réalité davantage de l’option stale-if-error, mais celle-ci reste en mémoire, donc tout disparaît si le processus redémarre.
Et sur la question de savoir si DiskLayer est vraiment nécessaire : dans la plupart des environnements multi-instances, Memory → Redis suffit largement, et dès que Disk s’ajoute comme couche, il faut aussi gérer le coût de sérialisation et la complexité de gestion des fichiers.