npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

layercache

v3.1.0

Published

Production-ready multi-layer caching for Node.js. Stack memory + Redis + disk behind one API with stampede prevention, tag invalidation, stale serving, and full observability.

Downloads

8,328

Readme


Why layercache?

// 100 concurrent requests hit an empty cache at the same time.
// Without stampede prevention, your DB gets 100 calls.
const results = await Promise.all(
  Array.from({ length: 100 }, () =>
    cache.get('user:1', () => db.findUser(1))
  )
)
// fetcherExecutions: 1  ← your DB was called exactly once

layercache is a multi-layer cache (Memory → Redis → Disk) for Node.js. Stampede prevention, tag invalidation, and distributed consistency are built in — no extra config required.


What's New in 3.0

  • RedisTagIndex uses 16 known-key shards by default. Existing Redis tag indexes that still use the legacy <prefix>:keys set should be migrated with npx layercache migrate-tag-index.
  • Production CLI commands reject plaintext redis:// URLs unless --allow-plaintext is passed. Prefer rediss:// for production Redis endpoints.
  • Express and Hono implicit URL cache keys now strip sensitive query parameters before caching, and non-2xx JSON responses are not cached by default.
  • Redis-backed generation persistence is available through RedisGenerationStore, and CacheStack.getGeneration() exposes the active generation.
  • The docs site now runs on Rspress and GitHub Pages.

See the changelog and migration guide before upgrading an existing deployment.


Quick Start

npm install layercache
import { CacheStack, MemoryLayer, RedisLayer } from 'layercache'
import Redis from 'ioredis'

const cache = new CacheStack([
  new MemoryLayer({ ttl: 60_000, maxSize: 1_000 }),       // L1: in-process
  new RedisLayer({ client: new Redis(), ttl: 3_600_000 }),  // L2: shared
])

// Read-through: fetcher runs once, all layers filled
const user = await cache.get('user:123', () => db.findUser(123))
const cache = new CacheStack([
  new MemoryLayer({ ttl: 60_000 })
])
import { CacheStack, MemoryLayer, RedisLayer, DiskLayer } from 'layercache'

const cache = new CacheStack([
  new MemoryLayer({ ttl: 60_000, maxSize: 5_000 }),
  new RedisLayer({ client: new Redis(), ttl: 3_600_000, compression: 'gzip' }),
  new DiskLayer({ directory: './var/cache', maxFiles: 10_000 }),
])

Performance

Environment: Node.js v20.20.1, Redis 7-alpine, Linux x86_64
CPU: AMD EPYC 4584PX 16-Core  |  RAM: 1.9 GB
Layers: MemoryLayer(ttl=60, maxSize=2000) + RedisLayer(ttl=300)
┌──────────────────────────────┬──────────┬──────────┬──────────┬──────────┐
│ Scenario                     │  avg ms  │  p95 ms  │  min ms  │  max ms  │
├──────────────────────────────┼──────────┼──────────┼──────────┼──────────┤
│ L1 memory hit (warm)         │   0.011  │   0.016  │   0.004  │   0.405  │
│ L1 hit in layered setup      │   0.006  │   0.007  │   0.004  │   0.077  │
│ No cache / origin fetch      │   6.844  │  11.196  │   4.683  │  11.196  │
└──────────────────────────────┴──────────┴──────────┴──────────┴──────────┘

┌──────────────────────────────┬────────────────────┐
│                              │  75 concurrent req │
├──────────────────────────────┼────────────────────┤
│ Without layercache           │  75 origin calls   │
│ With layercache              │   1 origin call    │  ← stampede prevention
└──────────────────────────────┴────────────────────┘

Benchmark commands and full scenario notes: docs/benchmarking.md


Migrating from node-cache-manager?

import { caching, multiCaching }
  from 'cache-manager'
import { redisStore }
  from 'cache-manager-redis-yet'

const mem = await caching('memory', {
  max: 100,
  ttl: 60 * 1000        // ms
})
const red = await caching(redisStore, {
  url: 'redis://localhost:6379',
  ttl: 300 * 1000       // ms
})
const cache = multiCaching([mem, red])

// stampede prevention:  ❌
// auto backfill:        ❌
// tag invalidation:     ❌
import {
  CacheStack,
  MemoryLayer,
  RedisLayer
} from 'layercache'
import Redis from 'ioredis'

const cache = new CacheStack([
  new MemoryLayer({ ttl: 60_000 }),    // ms
  new RedisLayer({
    client: new Redis(),
    ttl: 300_000                       // ms
  })
])

// stampede prevention:  ✅
// auto backfill:        ✅
// tag invalidation:     ✅

Full migration guides for keyv and cacheable.


Comparison

| | node-cache-manager | keyv | cacheable | BentoCache | layercache | |---|:---:|:---:|:---:|:---:|:---:| | Multi-layer + auto backfill | Partial | Plugin | -- | Partial | Yes | | Stampede prevention | -- | -- | -- | Partial | Yes | | Tag invalidation | -- | Yes | Yes | Yes | Yes | | TypeScript-first | Partial | Yes | Yes | Yes | Yes | | Event hooks | Yes | Yes | Yes | Yes | Yes |

| | node-cache-manager | keyv | cacheable | BentoCache | layercache | |---|:---:|:---:|:---:|:---:|:---:| | Multi-layer with auto backfill | Partial | Plugin | -- | Partial | Yes | | Stampede prevention | -- | -- | -- | Partial | Yes | | Distributed single-flight | -- | -- | -- | -- | Yes | | Tag invalidation | -- | Yes | Yes | Yes | Yes | | Distributed tags | -- | -- | -- | -- | Yes | | Cross-server L1 flush | -- | -- | -- | Yes | Yes | | Stale-while-revalidate | -- | -- | -- | Yes | Yes | | Circuit breaker | -- | -- | -- | Yes | Yes | | Graceful degradation | -- | -- | -- | Yes | Yes | | Sliding / adaptive TTL | -- | -- | -- | -- | Yes | | Cache warming | -- | -- | -- | -- | Yes | | Persistence / snapshots | -- | -- | -- | -- | Yes | | Compression | -- | -- | Yes | -- | Yes | | Admin CLI | -- | -- | -- | -- | Yes | | TypeScript-first | Partial | Yes | Yes | Yes | Yes | | Wrap / decorator API | Yes | -- | -- | Partial | Yes | | Namespaces | -- | Yes | Yes | Yes | Yes | | Event hooks | Yes | Yes | Yes | Yes | Yes | | Custom layers | Partial | -- | -- | Yes | Yes |

See the full comparison guide for detailed breakdowns.


Features

Core Caching

| Feature | What it does | |---|---| | Layered reads + auto backfill | Reads hit L1 first; on a partial hit, upper layers are filled automatically | | Stampede prevention | 100 concurrent requests for the same key = 1 fetcher execution | | Distributed single-flight | Cross-instance dedup via Redis locks with lease renewal | | Bulk operations | getMany() / setMany() / mdelete() with layer-level fast paths | | wrap() API | Transparent function caching with automatic key derivation | | Namespaces | Scoped cache views with hierarchical prefix support | | Cache warming | Pre-populate layers at startup with priority-based loading | | Negative caching | Cache misses (e.g., "user not found") for short TTLs | | Stored null values | cacheNullValues keeps intentional null values distinct from misses | | Entry introspection | getEntry() reports value, kind, state, key, and source layer |

Invalidation & Freshness

| Feature | What it does | |---|---| | Tag invalidation | Delete all keys with a given tag across all layers | | Batch tag invalidation | Multi-tag operations with any / all semantics | | Wildcard & prefix invalidation | Glob-style and hierarchical key patterns | | Generation-based rotation | Bulk namespace invalidation without scanning | | Stale-while-revalidate | Return cached value, refresh in background | | Stale-if-error | Keep serving stale when upstream fails | | Sliding TTL | Reset expiry on every read for frequently-accessed keys | | Adaptive TTL | Auto-ramp TTL for hot keys up to a ceiling | | Refresh-ahead | Proactively refresh before expiry | | TTL policies | Align expirations to calendar boundaries (until-midnight, next-hour, custom) | | Context-aware entry options | Derive TTLs and tags from the cached value right before storage |

Resilience & Operations

| Feature | What it does | |---|---| | Graceful degradation | Skip failed layers temporarily, keep cache available | | Circuit breaker | Stop hammering broken upstreams after repeated failures | | Shared circuit breaker scopes | Group failures by backend dependency with scope: 'shared' and breakerKey | | Fetcher rate limiting | Scoped to global, per-key, or per-fetcher; queueOverflow: 'reject' rejects saturated queues and 'bypass' runs overflow work directly | | Write policies | strict (fail if any layer fails) or best-effort | | Write-behind | Batch writes with configurable flush interval | | Bounded disk writes | DiskLayer.maxWriteQueueDepth prevents unbounded serialized write buildup | | Compression | gzip / brotli in RedisLayer with configurable threshold | | MessagePack | Pluggable serializers (JSON default, MessagePack alternative) | | Persistence | Export/import snapshots to memory or disk |

Observability

| Feature | What it does | |---|---| | Metrics | Hits, misses, fetches, stale hits, circuit breaker trips, and more | | Per-layer latency | Avg, max, and sample count using Welford's algorithm | | Health checks | Async health endpoint per layer with latency measurement | | Event hooks | hit, miss, set, delete, expire, stale-serve, stampede-dedupe, backfill, warm, error | | OpenTelemetry | Hook-based distributed tracing support without method monkey-patching | | Prometheus exporter | Metrics export including latency gauges | | HTTP stats handler | JSON endpoint for dashboards | | Admin CLI | npx layercache stats\|keys\|invalidate for Redis-backed caches |


Integrations

layercache plugs into the frameworks you already use:

| Framework | Integration | |---|---| | Express | createExpressCacheMiddleware(cache, opts) - auto-caches responses with x-cache: HIT/MISS header | | Fastify | createFastifyLayercachePlugin(cache, opts) - registers fastify.cache with optional stats route | | Hono | createHonoCacheMiddleware(cache, opts) - edge-compatible middleware | | tRPC | createTrpcCacheMiddleware(cache, prefix, opts) - procedure middleware | | GraphQL | cacheGraphqlResolver(cache, prefix, resolver, opts) - field resolver wrapper | | Next.js | Works natively with App Router and API routes | | OpenTelemetry | createOpenTelemetryPlugin(cache, tracer) - event-driven tracing spans without monkey-patching |

import { CacheStack, MemoryLayer, createExpressCacheMiddleware } from 'layercache'

const cache = new CacheStack([new MemoryLayer({ ttl: 60_000 })])

app.get('/api/users', createExpressCacheMiddleware(cache, {
  ttl: 30_000,
  tags: ['users'],
  keyResolver: (req) => `users:${req.url}`
}), async (req, res) => {
  res.json(await db.getUsers())
})
export async function GET(_req: Request, { params }: { params: { id: string } }) {
  const data = await cache.get(`user:${params.id}`, () => db.findUser(Number(params.id)))
  return Response.json(data)
}

Distributed Deployments

layercache is built for multi-instance production environments:

  ┌───────────┐    ┌───────────┐    ┌───────────┐
  │ Server A  │    │ Server B  │    │ Server C  │
  │ [Memory]  │    │ [Memory]  │    │ [Memory]  │
  └─────┬─────┘    └─────┬─────┘    └─────┬─────┘
        │                │                │
        └──── Redis Pub/Sub ──────────────┘  <-- L1 invalidation bus
                     │
               ┌─────┴──────┐
               │   Redis    │  <-- shared L2 + tag index + single-flight
               └────────────┘
  • Redis single-flight - dedup misses across instances with distributed locks
  • Redis invalidation bus - pub/sub-based L1 invalidation for memory consistency
  • Redis tag index - shared tag tracking with 16 known-key shards by default
  • Snapshot persistence - export/import state between instances
import {
  CacheStack, MemoryLayer, RedisLayer,
  RedisInvalidationBus, RedisTagIndex, RedisSingleFlightCoordinator
} from 'layercache'

const redis = new Redis(process.env.REDIS_URL)
const bus = new RedisInvalidationBus({
  publisher: redis,
  subscriber: new Redis(process.env.REDIS_URL),
  signingSecret: process.env.LAYERCACHE_INVALIDATION_SECRET
})
const tagIndex = new RedisTagIndex({ client: redis, prefix: 'myapp:tags', knownKeysShards: 16 })
const coordinator = new RedisSingleFlightCoordinator({ client: redis })

const cache = new CacheStack(
  [
    new MemoryLayer({ ttl: 60_000, maxSize: 10_000 }),
    new RedisLayer({ client: redis, ttl: 3_600_000, prefix: 'myapp:cache:' })
  ],
  {
    invalidationBus: bus,
    tagIndex: tagIndex,
    singleFlightCoordinator: coordinator,
    gracefulDegradation: { retryAfterMs: 10_000 }
  }
)

Documentation

| Document | Description | |---|---| | API Reference | Complete API documentation with all options | | Tutorial | Step-by-step operational walkthrough | | Comparison Guide | Detailed feature comparison with alternatives | | Migration Guide | Migrate from node-cache-manager, keyv, or cacheable | | Benchmarking | Benchmark scenarios and methodology | | Changelog | Version history and breaking changes |


Examples

The examples/ directory contains ready-to-run projects:


Requirements

  • Node.js >= 20
  • TypeScript >= 5.0 (optional - fully typed, ships .d.ts)
  • ioredis >= 5 (optional - only needed for Redis features)

Runtime dependencies: async-mutex and @msgpack/msgpack


Contributing

Contributions welcome - bug fixes, docs, performance, new adapters, or issues.

git clone https://github.com/flyingsquirrel0419/layercache
cd layercache
npm install
npm run lint && npm test && npm run build:all

See the Contributing Guide and Code of Conduct.


License

Apache 2.0 - use it freely in personal and commercial projects.