---
title: "@playstack/cache"
description: "JSON caching with millisecond TTLs, tag generations, stampede-safe reads, and explicit capabilities."
tags: ["package","infrastructure","cache","free"]
---

{/* package-access:start */}

> **Free.** MIT licensed. Check preview availability before installing. See [package access](/docs/packages#access-policy).

{/* package-access:end */}

`@playstack/cache` provides a small exact-JSON cache with millisecond TTLs, canonical tag generations, stampede-safe `remember`, explicit capabilities, and native client access.

{/* package-install:start */}

## Install

After confirming [preview access](/docs/packages#access-policy), install the package at your application's shared Playstack version:

```sh
npm install --save-exact @playstack/cache@0.1.0-beta.1
```

Check the peer requirements below before choosing a runtime or provider.

{/* package-install:end */}

## Compose and use

```ts
import { createCache } from '@playstack/cache'
import { RedisCacheStorage } from '@playstack/cache/redis'

const cache = createCache({
  app: 'billing',
  environment: 'production',
  driver: new RedisCacheStorage(redis),
  lockTtlMs: 30_000,
})

const plan = await cache.remember('plans:account-1', 60_000, () => loadPlan())

const accountCache = cache.tags('accounts', 'region:us')
await accountCache.put('summary', { total: 4 }, 60_000)
await accountCache.flush()
```

The physical namespace is `{app}:{environment}:cache`. Keys allow up to 512 UTF-8 bytes and no control characters. TTLs are positive integer milliseconds.

## Configuration reference

| Property             | Required | Purpose                                               |
| -------------------- | -------- | ----------------------------------------------------- |
| `app`, `environment` | Yes      | Canonical physical namespace.                         |
| `driver`             | Yes      | JSON entry, tag-generation, and optional lock bridge. |
| `lockTtlMs`          | No       | Distributed stampede lease; defaults to 30 seconds.   |

Values cross a versioned exact-JSON boundary. Class instances, undefined, non-finite numbers, sparse arrays, symbols, and cycles fail before the driver. Cached null is retained by `remember`, while `get<T>()` intentionally uses `T | null` for conventional misses.

## Tag and lock behavior

Tags are sorted and deduplicated. Tagged and untagged keys remain separate. `flush()` increments canonical tag generations, making old entries unreachable without scanning; they expire on their original TTL.

Concurrent `remember` calls on one cache instance always share a promise. A driver with atomic locks extends that across processes. Loader errors remain unchanged, are not cached, and release the lease.

Native lock-capable storage fences publication with `putIfLockOwner`: a loader whose lease expired cannot overwrite a successor's cached value. Its original caller can still receive the computed, uncached result; the loader is not automatically rerun. This is not an exactly-once execution guarantee.

## Batches, bounded memory, and observations

`getMany<T>(keys)` returns ordered values or nulls and preserves duplicate read keys. `putMany([{ key, value, ttlMs }])` validates and snapshots the whole batch before I/O and rejects duplicate write keys. Batches must be dense arrays of at most 1,000 entries; empty batches are no-ops. Tagged batches use one generation snapshot. Redis uses a native multi-get or Lua write; other drivers can fall back to single-key operations. A failed write may be partial or unacknowledged: there is no portable batch transaction or automatic retry.

`MemoryCacheStorage` is exported from the package root and can serve bounded process-local caches, not just tests. Its defaults are 1,000 values, 16 MiB of UTF-8 key/value bytes, 1,000 tags, and 1,000 locks. Pass positive integer `maxEntries`, `maxBytes`, `maxTags`, and `maxLocks` options as the second constructor argument (after an optional clock). The byte limit is not a total JavaScript heap bound.

Capacity pressure removes expired entries before least-recently-read values. An oversized value is rejected without evicting other values. `pruneExpired(budget = 100)` performs a bounded, resumable sweep without installing a timer. Live locks are not evicted, and tag capacity exhaustion fails rather than resetting generations and reviving stale values.

`cache.getStats()` returns an immutable storage-instance observation of hits, misses, acknowledged writes, and available eviction/expiration counts. These are not namespace-scoped or global request metrics. Unobservable remote counts and unsupported custom-driver statistics are `null`, not fabricated zeroes.

## Driver capabilities

| Driver                | Distributed lock         | Tag consistency |
| --------------------- | ------------------------ | --------------- |
| Bounded memory        | No, process-local        | Immediate       |
| Redis                 | Yes, token-checked lease | Immediate       |
| Cloudflare Workers KV | No, process-local        | Eventual        |

Each driver exposes `client` and capabilities. Redis uses `SET NX PX`, Lua release, atomic generation increments, and native millisecond expiry. Workers KV stores logical millisecond expiry while accepting its 60-second minimum physical cleanup.

## Bind shared Next.js caching

[`@playstack/cache-next`](/docs/packages/infrastructure/cache-next) adapts a configured cache to Next's stable incremental `cacheHandler` contract and the Next 16 Cache Components `cacheHandlers` contract. It preserves structured-clone values and streamed Cache Component entries at the framework boundary rather than weakening the core exact-JSON cache.

The package also supplies a timestamped HMAC revalidation handler for both App Router and Pages Router applications. Route placement, cache policy, authorization, and deployment-specific Redis lifecycle remain application-owned.

## Boundary

Cache is not authoritative application state, and provider consistency differences are not hidden. Storage/malformed-entry failures become retryable `CacheUnavailableError`. The application creates, observes, and closes the native client and chooses loaders that finish within the lock lease.

{/* package-reference:start */}

## API entry points and requirements

Reference snapshot: `@playstack/cache@0.1.0-beta.1`. Import only the entry point your runtime needs. Paths below are relative to the installed package; use **Go to Definition** in your editor to inspect exact parameters, return types and overloads. Do not import the declaration-file paths directly.

| Public entry point | Declaration file |
| --- | --- |
| `@playstack/cache` | `./dist/index.d.ts` |
| `@playstack/cache/errors` | `./dist/errors.d.ts` |
| `@playstack/cache/types` | `./dist/types.d.ts` |
| `@playstack/cache/testing` | `./dist/testing.d.ts` |
| `@playstack/cache/redis` | `./dist/redis.d.ts` |
| `@playstack/cache/cloudflare-kv` | `./dist/cloudflare-kv.d.ts` |
| `@playstack/cache/package.json` | No TypeScript declaration (asset or metadata export). |

Node.js engine requirement: `>=20`. This is not a claim that every entry point works in browsers or Workers.

### Peer dependencies

This package declares no peer dependencies. Its ordinary dependencies are resolved by the package manager.

For a complete first program, start with [Getting started](/docs/getting-started). For API lookup and partial-example conventions, see [Reading the reference](/docs/packages#reading-the-reference). Provider failures, lifecycle requirements and application responsibilities remain described in the guide above; types alone do not establish production safety.

{/* package-reference:end */}
