API Reference

Complete API documentation for layercache.

Table of Contents


CacheStack

The main class that orchestrates reads, writes, and invalidation across multiple cache layers.

Constructor

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

const cache = new CacheStack(layers, options?)

Parameters:

  • layers - CacheLayer[] - Array of cache layers, ordered from fastest (L1) to slowest (Ln)
  • options - CacheStackOptions - Optional configuration (see CacheStackOptions)

Read Operations

cache.get<T>(key, fetcher?, options?): Promise<T | null>

Reads through all layers in order. On a partial hit (found in L2 but not L1), backfills the upper layers automatically. On a full miss, runs the fetcher if provided.

// Without fetcher - returns null on miss
const user = await cache.get<User>('user:123')

// With fetcher - runs once on miss, fills all layers
const user = await cache.get<User>('user:123', () => db.findUser(123))

// With full options
const user = await cache.get<User>('user:123', () => db.findUser(123), {
  ttl: { memory: 30_000, redis: 600_000 },
  tags: ['user', 'user:123'],
  negativeCache: true,
  negativeTtl: 15_000,
  staleWhileRevalidate: 30_000,
  staleIfError: 300_000,
  ttlJitter: 5_000
})

cache.getOrThrow<T>(key, fetcher?, options?): Promise<T>

Like get(), but throws CacheMissError instead of returning null.

import { CacheMissError } from 'layercache'

try {
  const config = await cache.getOrThrow<Config>('app:config')
} catch (err) {
  if (err instanceof CacheMissError) {
    console.error(`Missing key: ${err.key}`)
  }
}

cache.mget<T>(entries): Promise<Array<T | null>>

Concurrent multi-key fetch. Uses layer-level getMany() fast paths when all entries are simple reads.

const [user1, user2] = await cache.mget([
  { key: 'user:1', fetch: () => db.findUser(1) },
  { key: 'user:2', fetch: () => db.findUser(2) },
])

cache.has(key): Promise<boolean>

Check if a key exists in any layer.

cache.ttl(key): Promise<number | null>

Get the remaining TTL in milliseconds for a key in the first layer that has it. Returns null if the key doesn't exist.

cache.inspect(key): Promise<CacheInspectResult | null>

Returns detailed metadata about a cache key for debugging.

const info = await cache.inspect('user:123')
// {
//   key: 'user:123',
//   foundInLayers: ['memory', 'redis'],
//   freshTtlMs: 45,
//   staleTtlMs: 75,
//   errorTtlMs: 345,
//   isStale: false,
//   tags: ['user', 'user:123']
// }

cache.getEntry<T>(key): Promise<CacheEntryResult<T> | null>

Reads a key and returns entry metadata instead of only the value. Use this when null is a valid cached value and you need to distinguish stored nulls, negative-cache entries, stale entries, and misses.

await cache.set('user:deleted', null)

const entry = await cache.getEntry('user:deleted')
// {
//   key: 'user:deleted',
//   value: null,
//   kind: 'value',
//   state: 'fresh',
//   layer: 'memory'
// }

const miss = await cache.getEntry('user:missing')
// null

cache.set() stores null values directly. cacheNullValues applies to read-through fetchers that return null, not to direct writes.

await cache.get('user:deleted', async () => null, { cacheNullValues: true })

Write Operations

cache.set<T>(key, value, options?): Promise<void>

Writes to all layers simultaneously.

await cache.set('user:123', user, {
  ttl: { memory: 60_000, redis: 600_000 },
  tags: ['user', 'user:123'],
  staleWhileRevalidate: { redis: 30_000 },
  staleIfError: { redis: 120_000 },
  ttlJitter: { redis: 5_000 }
})

// Uniform TTL across all layers
await cache.set('user:123', user, { ttl: 120_000, tags: ['user'] })

cache.mset<T>(entries): Promise<void>

Concurrent multi-key write.

cache.delete(key): Promise<void>

Delete one exact key from all layers.

cache.mdelete(keys): Promise<void>

Bulk delete exact keys.

cache.clear(): Promise<void>

Delete all keys from all layers.


Invalidation

cache.invalidateByKey(key): Promise<void>

Alias for cache.delete(key). Use it when you want the invalidateBy* naming style for one exact key.

await cache.invalidateByKey('user:123') // deletes only user:123

cache.invalidateByKeys(keys): Promise<void>

Alias for cache.mdelete(keys). Deletes only the exact keys provided.

await cache.invalidateByKeys(['user:123', 'user:123:posts'])

cache.invalidateByTag(tag): Promise<void>

Deletes every key stored with this tag across all layers.

await cache.set('user:123',       user,  { tags: ['user:123'] })
await cache.set('user:123:posts', posts, { tags: ['user:123'] })

await cache.invalidateByTag('user:123') // both keys gone

cache.invalidateByTags(tags, mode?): Promise<void>

Delete keys matching any or all of a set of tags.

await cache.invalidateByTags(['tenant:a', 'users'], 'all') // keys tagged with both
await cache.invalidateByTags(['users', 'posts'], 'any')    // keys tagged with either

cache.invalidateByPattern(pattern): Promise<void>

Glob-style deletion. Patterns must be non-empty, at most 1024 characters, and free of control characters.

await cache.invalidateByPattern('user:*')

cache.invalidateByPrefix(prefix): Promise<void>

Hierarchical prefix-based invalidation. Prefer this over glob when keys are hierarchical.

await cache.invalidateByPrefix('user:123:') // deletes user:123:profile, user:123:posts, ...

cache.expireByKey(key): Promise<void>

Marks one exact key as no longer fresh while keeping the cached value available for stale-while-revalidate / stale-if-error windows.

await cache.expireByKey('user:123') // expires only user:123

cache.expireByKeys(keys): Promise<void>

Expires only the exact keys provided without deleting their stored values.

await cache.expireByKeys(['user:123', 'user:123:posts'])

cache.expireByTag(tag): Promise<void>

Marks every key stored with this tag as no longer fresh while keeping the cached value available for stale-while-revalidate / stale-if-error windows.

await cache.set('user:123', user, {
  ttl: 60_000,
  staleWhileRevalidate: 30_000,
  tags: ['user:123']
})

await cache.expireByTag('user:123') // value remains, next read can serve stale and refresh

cache.expireByTags(tags, mode?): Promise<void>

Expire keys matching any or all of a set of tags without deleting the stored values.

await cache.expireByTags(['tenant:a', 'users'], 'all')
await cache.expireByTags(['users', 'posts'], 'any')

cache.expireByPattern(pattern): Promise<void>

Glob-style expiration. Matching envelope-backed entries keep their stale windows; plain layer values that do not carry layercache freshness metadata are left unchanged.

await cache.expireByPattern('user:*')

cache.expireByPrefix(prefix): Promise<void>

Hierarchical prefix-based expiration. Prefer this over glob when keys are hierarchical.

await cache.expireByPrefix('user:123:')

Wrapping & Namespaces

cache.wrap(prefix, fetcher, options?)

Wraps an async function so every call is transparently cached. The key is derived from function arguments unless you supply a keyResolver.

const getUser = cache.wrap('user', (id: number) => db.findUser(id))
const user = await getUser(123) // key -> "user:123"

// Custom key resolver
const getUser = cache.wrap(
  'user',
  (id: number) => db.findUser(id),
  { keyResolver: (id) => String(id), ttl: 300_000 }
)

cache.namespace(prefix): CacheNamespace

Returns a scoped view with the same full API. clear() only touches prefix:* keys.

const users = cache.namespace('users')
const posts = cache.namespace('posts')

await users.set('123', userData)    // stored as "users:123"
await users.clear()                 // only deletes "users:*"

// Nested namespaces
const tenant = cache.namespace('tenant:abc')
const tenantPosts = tenant.namespace('posts')
await tenantPosts.set('1', data)   // stored as "tenant:abc:posts:1"

Namespace prefixes must be non-empty, at most 256 characters, and free of control characters.


Warming & Persistence

cache.warm(entries, options?)

Pre-populate layers at startup. Higher priority values run first.

await cache.warm(
  [
    { key: 'config',  fetcher: () => db.getConfig(),  priority: 10 },
    { key: 'user:1',  fetcher: () => db.findUser(1),  priority: 5 },
    { key: 'user:2',  fetcher: () => db.findUser(2),  priority: 5 },
  ],
  { concurrency: 4, continueOnError: true }
)

cache.exportState() / cache.importState(snapshot)

In-memory snapshot transfer.

const snapshot = await cache.exportState()
await anotherCache.importState(snapshot)

cache.persistToFile(path) / cache.restoreFromFile(path)

Disk-based snapshot persistence. Restricted to process.cwd() by default (configurable via snapshotBaseDir).

await cache.persistToFile('./cache-snapshot.json')
await cache.restoreFromFile('./cache-snapshot.json')

Observability

cache.getMetrics(): CacheMetricsSnapshot

const { hits, misses, fetches, staleHits, refreshes, writeFailures } = cache.getMetrics()

cache.getStats(): CacheStatsSnapshot

Returns metrics, per-layer degradation state, and background refresh count.

const { metrics, layers, backgroundRefreshes } = cache.getStats()
// layers: [{ name, isLocal, degradedUntil }]

cache.captureMetrics(operation)

Runs an async operation and returns only the metrics emitted while that operation was active. Namespaces use this internally so overlapping namespace operations do not serialize on a global metrics lock.

const { result, metrics } = await cache.captureMetrics(async () => {
  return cache.get('user:123', fetchUser)
})

If the operation rejects, the thrown error is annotated with a metrics property containing the captured CacheMetricsSnapshot.

try {
  await cache.captureMetrics(async () => cache.get('user:123', fetchUser))
} catch (error) {
  const metrics = (error as { metrics?: CacheMetricsSnapshot }).metrics
  throw error
}

cache.getHitRate()

Computed hit rate overall and per-layer.

cache.healthCheck(): Promise<CacheHealthCheckResult[]>

const health = await cache.healthCheck()
// [{ layer: 'memory', healthy: true, latencyMs: 0.03 }, ...]

cache.resetMetrics(): void

Resets all counters to zero.


Generation Management

Add a generation prefix to every key and rotate it for bulk invalidation without scanning.

const cache = new CacheStack([...], { generation: 1 })

await cache.set('user:123', user)
cache.bumpGeneration() // now reads use v2:user:123

// Optional: auto-cleanup old generation keys
const cache = new CacheStack([...], {
  generation: 1,
  generationCleanup: { batchSize: 500 }
})

Persist the active generation outside the process when you deploy multiple instances or restart workers:

import { CacheStack, RedisGenerationStore } from 'layercache'

const generations = new RedisGenerationStore({ client: redis })
const generation = await generations.getOrInitialize(1)

const cache = new CacheStack([...], { generation })

// Later, atomically rotate all future keys and apply that generation locally.
const nextGeneration = await generations.bump()
cache.bumpGeneration(nextGeneration)

cache.bumpGeneration()

Rotate cache namespace by incrementing generation.

cache.getGeneration()

Get current generation number.


Lifecycle

cache.disconnect(): Promise<void>

Graceful shutdown (unsubscribes from invalidation bus, etc.).


Cache Layers

All layers implement the CacheLayer interface:

interface CacheLayer {
  readonly name: string
  readonly defaultTtl?: number
  readonly isLocal?: boolean

  get<T>(key: string): Promise<T | null>
  getEntry?<T>(key: string): Promise<unknown | null>
  getMany?<T>(keys: string[]): Promise<Array<unknown | null>>
  set(key: string, value: unknown, ttl?: number): Promise<void>
  setMany?(entries: Array<{ key: string; value: unknown; ttl?: number }>): Promise<void>
  delete(key: string): Promise<void>
  deleteMany?(keys: string[]): Promise<void>
  clear(): Promise<void>
  keys?(): Promise<string[]>
  forEachKey?(visitor: (key: string) => void | Promise<void>): Promise<void>
  has?(key: string): Promise<boolean>
  ttl?(key: string): Promise<number | null>
  size?(): Promise<number>
  ping?(): Promise<boolean>
  dispose?(): Promise<void>
}

MemoryLayer

In-process LRU/LFU/FIFO eviction with configurable max size.

new MemoryLayer({
  ttl: 60_000,
  maxSize: 5_000,
  name: 'memory'    // default
})

RedisLayer

Distributed caching via ioredis with compression, serializers, and optional prefix.

new RedisLayer({
  client: redis,
  ttl: 300_000,
  prefix: 'myapp:cache:',
  compression: 'gzip',
  compressionThreshold: 1_024,
  commandTimeoutMs: 200,
  serializer: new MsgpackSerializer(),
  name: 'redis',
  allowUnprefixedClear: false
})

commandTimeoutMs applies a per-command timeout to Redis round-trips. When a Redis command exceeds this threshold, the layer surfaces an error so CacheStack can trigger graceful degradation instead of waiting on a slow dependency indefinitely.

DiskLayer

Persistent file-based caching with atomic writes and optional at-rest protection.

import { resolve } from 'node:path'

new DiskLayer({
  directory: resolve('./var/cache/layercache'),
  maxFiles: 50_000,
  maxWriteQueueDepth: 10_000,
  name: 'disk'
})

maxWriteQueueDepth caps pending serialized set() / delete() work so a slow disk cannot accumulate unbounded writes. Defaults to 10,000. Set it to false to disable the guard for trusted low-volume environments.

At-Rest Protection

DiskLayer supports AES-256-GCM encryption or HMAC-SHA256 signing to protect cached data on disk:

new DiskLayer({
  directory: resolve('./var/cache/layercache'),
  encryptionKey: process.env.CACHE_ENCRYPTION_KEY,  // AES-256-GCM encryption
  signingKey: process.env.CACHE_SIGNING_KEY,         // HMAC-SHA256 signing (ignored if encryptionKey is set)
  name: 'disk'
})

Encryption also provides authenticated integrity — a separate signingKey is unnecessary when encryptionKey is provided.

MemcachedLayer

Memcached support with pluggable serializers and bulk operations.

new MemcachedLayer({
  client: memcachedClient,
  ttl: 300_000,
  name: 'memcached'
})

Custom Layers

Implement CacheLayer to plug in any backend:

class MyCustomLayer implements CacheLayer {
  readonly name = 'custom'
  readonly defaultTtl = 300_000
  readonly isLocal = false

  async get<T>(key: string): Promise<T | null> { /* ... */ }
  async set(key: string, value: unknown, ttl?: number): Promise<void> { /* ... */ }
  async delete(key: string): Promise<void> { /* ... */ }
  async clear(): Promise<void> { /* ... */ }
}

Options Reference

CacheStackOptions

OptionTypeDefaultDescription
loggerLogger | booleanfalsePluggable logger interface or boolean
metricsbooleantrueEnable/disable metrics collection
stampedePreventionbooleantrueIn-process request deduplication
stampedeMaxInFlightnumber-Max concurrent in-flight deduplicated requests
stampedeEntryTimeoutMsnumber-Per-entry timeout for stampede guard
invalidationBusRedisInvalidationBus-Distributed L1 invalidation
tagIndexTagIndex | RedisTagIndexin-memoryCustom tag tracking
generationnumber-Generation prefix for bulk invalidation
generationCleanupboolean | { batchSize: number }-Auto-prune stale generation keys
broadcastL1InvalidationbooleanfalsePublish writes to peer memory layers
negativeCachingbooleanfalseCache nulls globally
cacheNullValuesbooleanfalseCache null fetcher results as regular values
negativeTtlnumber | LayerTtlMap-Global TTL for negative cache entries
staleWhileRevalidatenumber | LayerTtlMap-Global stale-while-revalidate window (milliseconds)
staleIfErrornumber | LayerTtlMap-Global stale-if-error window (milliseconds)
ttlJitternumber | LayerTtlMap-Global TTL jitter (milliseconds)
refreshAheadnumber | LayerTtlMap-Global refresh-ahead threshold (milliseconds)
adaptiveTtlboolean | AdaptiveTtlOptions-Auto-ramp TTLs for hot keys
circuitBreakerCircuitBreakerOptions-Per-fetcher failure tracking
gracefulDegradationboolean | { retryAfterMs: number }-Skip failed layers temporarily
writePolicy'strict' | 'best-effort''strict'Write failure behavior
writeStrategy'write-through' | 'write-behind''write-through'Write batching strategy
writeBehindWriteBehindOptions-Batch size, flush interval, max queue
fetcherRateLimitRateLimitOptions-Global rate limiting
backgroundRefreshTimeoutMsnumber30000Max time for stale refresh attempts
singleFlightCoordinatorRedisSingleFlightCoordinator-Distributed deduplication
singleFlightLeaseMsnumber30000Distributed lock duration
singleFlightTimeoutMsnumber5000Wait timeout for distributed lock
singleFlightPollMsnumber50Polling interval
singleFlightRenewIntervalMsnumber-Lease renewal cadence
snapshotBaseDirstring | falseprocess.cwd()Base directory for file snapshots
snapshotMaxBytesnumber | false-Max snapshot file size
snapshotMaxEntriesnumber | false-Max entries in a snapshot
invalidationMaxKeysnumber | false-Safety limit for invalidation scans
maxProfileEntriesnumber100000Max size before pruning internal maps

CircuitBreakerOptions

OptionTypeDefaultDescription
failureThresholdnumber3Consecutive failures before opening the circuit
cooldownMsnumber30000Milliseconds before another fetch attempt is allowed
scope'key' | 'shared''key'Use per-key buckets or one shared bucket for these options
breakerKeystring-Explicit bucket id for grouping related backend dependencies

If breakerKey is provided, it selects the explicit bucket id and takes precedence over scope. Otherwise scope: 'key' creates one bucket per cache key, while scope: 'shared' uses one shared bucket for all keys using those options.

await cache.get('user:1', fetchFromApi, {
  circuitBreaker: {
    failureThreshold: 2,
    cooldownMs: 60_000,
    scope: 'shared',
    breakerKey: 'users-api'
  }
})

RateLimitOptions

OptionTypeDefaultDescription
maxConcurrentnumber-Maximum concurrent fetchers in the selected bucket
intervalMsnumber-Rate-limit window size in milliseconds
maxPerIntervalnumber-Maximum fetches per interval
scope'global' | 'key' | 'fetcher''global'Bucket by all fetches, cache key, or fetcher function
bucketKeystring-Explicit bucket id for related work
queueOverflow'reject' | 'bypass''reject'Reject saturated queues or deliberately bypass the limiter

Per-Operation Options

OptionTypeDescription
tagsstring[]Tags for tag-based invalidation
ttlnumber | LayerTtlMapTTL in milliseconds, or per-layer overrides
ttlPolicystring | object | function'until-midnight', 'next-hour', { alignTo }, or custom
negativeCachebooleanCache null results
cacheNullValuesbooleanCache null fetcher results as regular values
negativeTtlnumberShort TTL for misses
staleWhileRevalidatenumber | LayerTtlMapReturn stale and refresh in background
staleIfErrornumber | LayerTtlMapKeep serving stale if refresh fails
ttlJitternumber | LayerTtlMap+/- random jitter on expiry
slidingTtlbooleanReset TTL on every read
refreshAheadnumberTrigger background refresh when TTL drops below threshold
adaptiveTtlAdaptiveTtlOptionsAuto-ramp TTL for hot keys
circuitBreakerCircuitBreakerOptionsPer-operation circuit breaker
fetcherRateLimitRateLimitOptionsPer-operation rate limiting
contextOptions(context) => CacheEntryWriteOptionsOverride stored entry TTLs/tags from { key, value, kind } right before write
shouldCache(value: T) => booleanPredicate to skip caching specific results

Invalidation Strategies

Tag Invalidation

await cache.set('user:123', user, { tags: ['user', 'user:123'] })
await cache.invalidateByTag('user:123')

Exact-Key Invalidation

await cache.invalidateByKey('user:123') // alias for delete()
await cache.invalidateByKeys(['user:123', 'user:456']) // alias for mdelete()

Batch Tag Invalidation

await cache.invalidateByTags(['tenant:a', 'users'], 'all')
await cache.invalidateByTags(['users', 'posts'], 'any')

Wildcard Invalidation

await cache.invalidateByPattern('user:*')

Prefix Invalidation

await cache.invalidateByPrefix('user:123:')

Expiration Without Deletion

Use the expireBy* counterparts when stale serving is preferable to removing values immediately.

await cache.expireByTag('user:123')
await cache.expireByKey('user:123')
await cache.expireByTags(['tenant:a', 'users'], 'all')
await cache.expireByKeys(['user:123', 'user:456'])
await cache.expireByPattern('user:*')
await cache.expireByPrefix('user:123:')

Generation-Based Invalidation

cache.bumpGeneration() // instant bulk invalidation without scanning

Freshness Strategies

Stale-While-Revalidate

await cache.set('config', config, {
  ttl: 60_000,
  staleWhileRevalidate: 30_000,  // serve stale for 30s while refreshing
  staleIfError: 300_000           // serve stale for 5min if refresh fails
})

Sliding TTL

await cache.get('session:abc', fetchSession, { slidingTtl: true })

Adaptive TTL

await cache.get('popular-post', fetchPost, {
  adaptiveTtl: { hotAfter: 5, step: 60_000, maxTtl: 3_600_000 }
})

Adaptive TTL counters are process-local. In multi-instance deployments, each Node.js process ramps TTLs from its own observed hits, so use explicit TTLs or a shared Redis counter when every instance must make the same TTL decision.

Refresh-Ahead

await cache.get('leaderboard', fetchLeaderboard, {
  ttl: 120_000,
  refreshAhead: 30_000 // refresh when <= 30s remain
})

TTL Policies

await cache.set('daily-report', report, { ttlPolicy: 'until-midnight' })
await cache.set('hourly-rollup', rollup, { ttlPolicy: 'next-hour' })
await cache.set('aligned', value, { ttlPolicy: { alignTo: 300_000 } })
await cache.set('custom', value, {
  ttlPolicy: ({ key }) => key.startsWith('hot:') ? 30_000 : 300_000
})

Context-Aware Entry Options

await cache.get('oauth:token', fetchToken, {
  ttl: 300_000,
  contextOptions: ({ value }) => {
    const token = value as { refreshExpiresInMs: number; tenantId: string }
    return {
      ttl: Math.max(1, token.refreshExpiresInMs),
      tags: ['oauth', `tenant:${token.tenantId}`]
    }
  }
})

contextOptions() runs immediately before a cache write and overrides static entry settings on the same call. Use it for value-dependent ttl, negativeTtl, staleWhileRevalidate, staleIfError, ttlJitter, adaptiveTtl, or tags.

Per-Layer TTL Overrides

await cache.set('session:abc', data, {
  ttl: { memory: 30_000, redis: 3_600_000 }
})

Conditional Caching

const data = await cache.get('api:response', fetchFromApi, {
  shouldCache: (value) => (value as any).status === 200
})

Resilience

Graceful Degradation

new CacheStack([...], {
  gracefulDegradation: { retryAfterMs: 10_000 }
})

Circuit Breaker

new CacheStack([...], {
  circuitBreaker: { failureThreshold: 5, cooldownMs: 30_000 }
})

// Per-operation
await cache.get('fragile-key', fetch, {
  circuitBreaker: { failureThreshold: 3, cooldownMs: 10_000 }
})

Write Policies

// Strict (default): fail if any layer fails
new CacheStack([...], { writePolicy: 'strict' })

// Best-effort: only fail if every layer fails
new CacheStack([...], { writePolicy: 'best-effort' })

Scoped Fetcher Rate Limiting

await cache.get('user:123', fetchUser, {
  fetcherRateLimit: { maxConcurrent: 1, scope: 'key' }
})

Compression & Serialization

Compression

new RedisLayer({
  client: redis,
  ttl: 300_000,
  compression: 'gzip',         // or 'brotli'
  compressionThreshold: 1_024  // skip compression for small values
})

For large payloads such as HTML fragments, denormalized API responses, or MB-scale documents, prefer compression: 'brotli' with a threshold around 1_024 * 1_024 so small values avoid compression overhead while large values pay less network cost.

MessagePack Serializer

import { MsgpackSerializer } from 'layercache'

new RedisLayer({
  client: redis,
  ttl: 300_000,
  serializer: new MsgpackSerializer()
})

Distributed Features

Distributed Single-Flight

import { RedisSingleFlightCoordinator } from 'layercache'

const coordinator = new RedisSingleFlightCoordinator({ client: redis })

new CacheStack([...], {
  singleFlightCoordinator: coordinator,
  singleFlightLeaseMs: 30_000,
  singleFlightRenewIntervalMs: 10_000,
})

Cross-Server L1 Invalidation

import { RedisInvalidationBus } from 'layercache'

const bus = new RedisInvalidationBus({
  publisher: redis,
  subscriber: new Redis(),
  signingSecret: process.env.LAYERCACHE_INVALIDATION_SECRET
})

new CacheStack([...], {
  invalidationBus: bus,
  broadcastL1Invalidation: true
})

Distributed Tag Index

import { RedisTagIndex } from 'layercache'

const tagIndex = new RedisTagIndex({
  client: redis,
  prefix: 'myapp:tag-index',
  knownKeysShards: 16
})

new CacheStack([...], { tagIndex })

Event Hooks

CacheStack extends EventEmitter:

EventPayload
hit{ key, layer }
miss{ key }
set{ key }
delete{ key }
stale-serve{ key, state, layer }
stampede-dedupe{ key }
backfill{ key, fromLayer, toLayer }
warm{ key }
error{ event, context }
cache.on('hit',   ({ key, layer }) => metrics.inc('cache.hit', { layer }))
cache.on('miss',  ({ key })        => metrics.inc('cache.miss'))
cache.on('error', ({ event, context }) => logger.error(event, context))

Framework Integrations

Express

import { createExpressCacheMiddleware } from 'layercache'

app.get('/api/users', createExpressCacheMiddleware(cache, {
  ttl: 30_000,
  tags: ['users'],
  keyResolver: (req) => `user:${req.url}`
}), handler)

Fastify

import { createFastifyLayercachePlugin } from 'layercache'

await fastify.register(createFastifyLayercachePlugin(cache, {
  statsPath: '/cache/stats'
}))

Hono

import { createHonoCacheMiddleware } from 'layercache'

app.use('/api/*', createHonoCacheMiddleware(cache, { ttl: 60_000 }))

tRPC

import { createTrpcCacheMiddleware } from 'layercache'

const cacheMiddleware = createTrpcCacheMiddleware(cache, 'trpc', { ttl: 60_000 })
export const cachedProcedure = t.procedure.use(cacheMiddleware)

GraphQL

import { cacheGraphqlResolver } from 'layercache'

const resolvers = {
  Query: {
    user: cacheGraphqlResolver(cache, 'user', (_root, { id }) => db.findUser(id), {
      keyResolver: (_root, { id }) => id,
      ttl: 300_000
    })
  }
}

NestJS

Use CacheStack directly in your NestJS modules:

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

@Module({
  providers: [
    {
      provide: 'CACHE',
      useFactory: () => new CacheStack([
        new MemoryLayer({ ttl: 60_000 }),
        new RedisLayer({ client: new Redis(), ttl: 300_000 })
      ])
    }
  ],
  exports: ['CACHE']
})
export class CacheModule {}

Note: The separate @cachestack/nestjs package was removed in v1.3.2. Import directly from layercache instead.

OpenTelemetry

import { createOpenTelemetryPlugin } from 'layercache'

createOpenTelemetryPlugin(cache, tracer)

Stats HTTP Handler

import { createCacheStatsHandler } from 'layercache'
import http from 'node:http'

const statsHandler = createCacheStatsHandler(cache)
http.createServer(statsHandler).listen(9090)

Admin CLI

Inspect and manage Redis-backed caches from the terminal.

npx layercache stats      --redis redis://localhost:6379
npx layercache keys       --redis redis://localhost:6379 --pattern "user:*"
npx layercache invalidate --redis redis://localhost:6379 --tag user:123
npx layercache invalidate --redis redis://localhost:6379 --pattern "session:*"

Debug Logging

DEBUG=layercache:debug node server.js

Or pass a logger instance:

new CacheStack([...], {
  logger: {
    debug(message, context) { myLogger.debug(message, context) }
  }
})