Skip to content
Warlock.js v4

Built-in Metrics

cache.metrics() returns a running snapshot of cache health — counters, hit-rate, latency percentiles, per-driver breakdowns. Built-in to the manager, no setup, no external deps.

const m = cache.metrics();
// {
// hits: 9821, misses: 173, sets: 412, removed: 18, errors: 0,
// hitRate: 0.983,
// latencyMs: { p50: 0.4, p95: 2.1, p99: 8.2, samples: 1000 },
// byDriver: { memory: { ... }, redis: { ... } },
// startedAt: 1714185600000,
// }
FieldSource
hits, misseshit / miss events from drivers
setsset event fires on every successful write
removedremoved event on cache.remove(key)
errorserror event (driver failures, SWR background refresh failures, etc.)
hitRateComputed at snapshot time: hits / (hits + misses)
latencyMs.p50/p95/p99Sampled by the manager around every get / set / remove
byDriverSame fields broken out per driver name
startedAtMillisecond timestamp the collector last reset

The collector subscribes to events the first time you call cache.metrics() or cache.resetMetrics(). Apps that never read metrics pay zero cost — no listeners, no allocations, no latency sampling.

Once attached, the collector survives cache.use() driver switches because it listens on the manager’s global event bus, which re-attaches handlers to every loaded driver.

// First call attaches the collector. Earlier events are NOT counted.
cache.metrics();
// Subsequent ops are tracked.
await cache.set("k", "v");
await cache.get("k");
// Read updated counts.
const snapshot = cache.metrics();

Tip. If you want metrics on every op including the first, call cache.metrics() once during app startup right after cache.init().

Latency is sampled by the manager’s wrapper around get / set / remove. Each sample is appended to a circular buffer (default size 1000) per driver — older samples are overwritten when the buffer is full. Percentiles are computed at snapshot time by sorting a copy of the buffer.

This means percentiles reflect “the most recent ~1000 ops,” not the lifetime distribution. That’s the right tradeoff for live dashboards: a slow stretch shows up immediately and ages out as traffic recovers.

cache.resetMetrics() zeroes counters, drops every latency sample, and sets startedAt to now. Useful for tests, for boundary measurements (deploy, traffic burst), or for periodic export pipelines that “tail” the snapshot:

setInterval(() => {
const snapshot = cache.metrics();
exporter.send(snapshot);
cache.resetMetrics();
}, 60_000);

When traffic flows through more than one driver (manual cache.use("redis"), per-call set(k, v, { driver: "memory" })), each one gets its own bucket:

const m = cache.metrics();
console.log(`memory hit rate: ${m.byDriver.memory?.hitRate ?? 0}`);
console.log(`redis p95: ${m.byDriver.redis?.latencyMs.p95 ?? 0}ms`);

Drivers that never fire events stay absent from byDriver.

import client from "prom-client";
const hits = new client.Counter({ name: "cache_hits_total", labelNames: ["driver"] });
const misses = new client.Counter({ name: "cache_misses_total", labelNames: ["driver"] });
const latency = new client.Histogram({
name: "cache_op_seconds",
labelNames: ["driver"],
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1],
});
setInterval(() => {
const snapshot = cache.metrics();
for (const [driver, row] of Object.entries(snapshot.byDriver)) {
hits.inc({ driver }, row.hits);
misses.inc({ driver }, row.misses);
}
cache.resetMetrics();
}, 10_000);
import StatsD from "hot-shots";
const statsd = new StatsD();
setInterval(() => {
const snapshot = cache.metrics();
statsd.gauge("cache.hit_rate", snapshot.hitRate);
statsd.gauge("cache.p95_ms", snapshot.latencyMs.p95);
statsd.gauge("cache.p99_ms", snapshot.latencyMs.p99);
cache.resetMetrics();
}, 10_000);
setInterval(() => {
const m = cache.metrics();
console.log(
`[cache] hitRate=${(m.hitRate * 100).toFixed(1)}% ` +
`p95=${m.latencyMs.p95.toFixed(2)}ms errors=${m.errors}`,
);
}, 60_000);

The metrics collector is the right answer when you want aggregate observability — “how is the cache doing overall.” For per-event reactions (alerting on individual errors, audit-logging specific keys), use the event system directly:

cache.on("error", ({ key, error }) => {
pagerDuty.trigger(`Cache error on ${key}`, error);
});

Both can coexist — events fire whether the metrics collector is attached or not.