Skip to content

Observability

Documentation · API reference

Metrics are disabled unless a DialCacheMetricsAdapter is passed to the constructor. new DialCache() does not import a metrics backend, register collectors, or emit metrics.

DialCache provides first-party adapters for Prometheus and Datadog. Both use caller-created, caller-owned clients and preserve one backend-neutral set of bounded labels.

Choose Prometheus or Datadog for setup. The metric catalog defines shared semantics; the outcome tables below it explain individual feature signals.

Reading the signals

Start with source load and caller latency, then explain changes with the cache metrics. An earlier-layer hit stops traversal; a coalesced follower does not repeat the leader's full read/miss trail.

SignalInterpretation
Requests and misses by layerWhich layer actually serves or falls through
Miss reasonAbsence, logical expiry, invalidation fencing, or an unclassified miss
Disabled reasonIntentional policy/ramp skips versus invalid configuration
Errors and fallback durationDependency failures and source cost, including recovered source failures
Shadow outcomes and value agesComparison verdicts, fill activity, drops, and detached failures
Recovery outcomes and value agesHow often an older snapshot serves during eligible source failures
Compression size, ratio, and durationPrepared payload savings versus synchronous CPU cost
Future timestamp offsetObserved frames ahead of the reader clock; an incomplete clock-health signal

Durations and ages use seconds; sizes use bytes. Namespace, use case, and key type should remain bounded application-defined labels. No metric includes cache ids, arguments, payloads, or raw error text.

Miss reasons

miss() receives MissMetricLabels with one required reason. Both bundled backends emit the same bounded values:

reasonMeaning
value_absentNo retrievable value: never populated, physically expired, evicted, Redis nil, or tracked MGET wrong-type-as-nil. All local misses use this reason.
expiredA supported valid non-future Redis frame reached its logical fresh age, including retained stale candidates and frames beyond the recovery maximum.
watermark_fencedA supported positive-timestamp tracked frame was rejected at or below a valid observed watermark, before deserialization.
unclassifiedOther real misses, including unknown adapter results, malformed frames/metadata, invalid or future timestamps, and deserialization failures.

Read errors and timeouts are errors, not ordinary misses. The observed watermark used for refill suppression is separate from the reason; a missing value can carry a valid fence.

Prometheus

Install prom-client separately:

bash
npm install prom-client@^15.1.3

Create the registry your application owns, then pass an explicit adapter to DialCache:

ts
import { Registry } from "prom-client";
import { DialCache } from "dialcache";
import { createPrometheusDialCacheMetrics } from "dialcache/prometheus";

const registry = new Registry();

const dialcache = new DialCache({
  namespace: "users-api",
  metrics: createPrometheusDialCacheMetrics({
    registry,
    prefix: "myapp_",
  }),
});

app.get("/metrics", async (_req, res) => {
  res.type(registry.contentType).send(await registry.metrics());
});

The adapter requires a caller-owned Registry. It never uses the global default registry, and it does not clear or otherwise own the registry lifecycle. prefix defaults to "" and is concatenated literally with each metric name; include any desired separator yourself.

Multiple adapters with the same registry and prefix reuse existing collectors when their type, help, labels, histogram buckets, and exemplar mode match. Adapter construction fails before registering anything if a same-name collector has an incompatible schema. Use a unique prefix or separate registry to resolve a collision. DialCache's collectors do not enable exemplars, so an exemplar-enabled collector with the same name is incompatible.

Histogram buckets

Bucket boundaries are fixed; the adapter has no bucket customization option:

Metric familyUnitFinite bucket boundaries
All timersSeconds0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10
Serialized and stored sizesBytes100, 1000, 10000, 100000, 1000000, 10000000
Compression ratioRatio0.05, 0.1, 0.2, 0.3, 0.5, 0.7, 0.9, 1
Shadow and recovery value agesSeconds1, 5, 15, 60, 300, 900, 3600, 10800, 43200, 86400, 259200, 604800
Future timestamp offsetsSeconds0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 5, 15, 60, 300, 900, 3600, 10800, 43200

Prometheus metrics

See the metric catalog for names, types, labels, and meanings.

Datadog

Install hot-shots separately:

bash
npm install hot-shots@^17.0.0

Create the DogStatsD client your application owns, then pass it to the Datadog adapter:

ts
import StatsD from "hot-shots";
import { DialCache } from "dialcache";
import { createDatadogDialCacheMetrics } from "dialcache/datadog";

const dogStatsD = new StatsD({
  host: process.env.DD_AGENT_HOST ?? "127.0.0.1",
  globalTags: {
    service: "users-api",
    env: process.env.DD_ENV ?? "development",
  },
  errorHandler: (error) =>
    logger.warn("DogStatsD error", { error }),
});

const dialcache = new DialCache({
  namespace: "users-api",
  metrics: createDatadogDialCacheMetrics({
    client: dogStatsD,
    observationMetricType: "distribution",
    namespace: "dialcache",
  }),
});

function shutdown(): void {
  // Close the client yourself once outstanding DialCache calls have settled.
  // DialCache never flushes or closes it.
  dogStatsD.close();
}

hot-shots is the supported and tested client, but the adapter depends only on the exported DatadogDogStatsDClient structural interface. Construction requires all three methods, increment, histogram, and distribution, to be functions, regardless of the selected observation mode.

DialCache does not:

  • import or install hot-shots;
  • create a client;
  • flush buffers;
  • close sockets; or
  • otherwise own the client lifecycle.

Distribution or histogram

observationMetricType is required.

Choose "distribution" when latency and size percentiles must aggregate across hosts. Enable the desired distribution percentiles and aggregations in Datadog.

Choose "histogram" when host-level histogram aggregation matches the existing Datadog setup. The choice applies uniformly to every duration, size, ratio, age, and offset observation emitted by the adapter. Both modes produce Datadog custom metrics.

Metric volume depends on tag combinations and selected aggregations. Review Datadog's custom-metrics billing guidance before rollout.

Do not send both observation types under the same metric namespace. When changing types, use a new namespace during migration so one metric identity never mixes histogram and distribution points.

Datadog namespaces

DatadogMetricsOptions.namespace is the metric-name namespace and defaults to dialcache. It is separate from DialCacheConfig.namespace, the logical cache namespace emitted as the cache_namespace tag.

The Datadog metric namespace must:

  • start with a letter;
  • contain only letters, numbers, underscores, and dot-separated non-empty segments; and
  • produce final metric names no longer than 200 characters.

The adapter rejects invalid namespaces and overlong final names instead of relying on client-side normalization.

A hot-shots prefix is applied after the adapter constructs the name. Include that prefix when checking final length, and avoid accidentally combining it with the adapter namespace. Client-level globalTags are appended by hot-shots; the table below lists only tags added by DialCache.

Datadog metrics

The adapter emits exact increments of 1 for counters and preserves seconds and bytes without unit conversion:

Datadog metricTypePrometheus equivalent
dialcache.request.countCountdialcache_request_counter
dialcache.miss.countCountdialcache_miss_counter
dialcache.disabled.countCountdialcache_disabled_counter
dialcache.error.countCountdialcache_error_counter
dialcache.invalidation.countCountdialcache_invalidation_counter
dialcache.coalesced.countCountdialcache_coalesced_counter
dialcache.shadow.countCountdialcache_shadow_validation_counter
dialcache.shadow.value_ageDistribution or histogramdialcache_shadow_value_age_histogram
dialcache.future_timestamp_offsetDistribution or histogramdialcache_future_timestamp_offset_histogram
dialcache.stale_recovery.countCountdialcache_stale_recovery_counter
dialcache.stale_recovery.value_ageDistribution or histogramdialcache_stale_recovery_value_age_histogram
dialcache.compression.countCountdialcache_compression_counter
dialcache.get.durationDistribution or histogramdialcache_get_timer
dialcache.fallback.durationDistribution or histogramdialcache_fallback_timer
dialcache.serialization.durationDistribution or histogramdialcache_serialization_timer
dialcache.serialization.sizeDistribution or histogramdialcache_size_histogram
dialcache.stored.sizeDistribution or histogramdialcache_stored_size_histogram
dialcache.compression.ratioDistribution or histogramdialcache_compression_ratio_histogram
dialcache.compression.durationDistribution or histogramdialcache_compression_timer

Labels and meanings are shared with the metric catalog.

Synchronous client throws are isolated when DialCache invokes the adapter. DialCache also consumes thenables returned by adapter hooks, but this adapter does not forward every client return value: only shadowValidation and staleRecovery return the counter call's result. A custom DogStatsD client must handle its own asynchronous delivery failures, including rejected promises. Direct adapter calls do not have DialCache's observer guard. Configure client error handling and shutdown as part of application ownership.

Metric catalog

This table uses Prometheus names and types, without the optional prefix. Datadog metrics map to the same observations and labels.

MetricTypeLabelsDescription
dialcache_request_counterCountercache_namespace, use_case, key_type, layerCache-layer requests that reached an enabled layer
dialcache_miss_counterCountercache_namespace, use_case, key_type, layer, reasonCache misses, classified by one required bounded reason
dialcache_disabled_counterCountercache_namespace, use_case, key_type, layer, reasonCache skips (context, policy_disabled, invalid_ttl, invalid_ramp, ramped_down, config_error)
dialcache_error_counterCountercache_namespace, use_case, key_type, layer, error, in_fallbackCache/fallback errors and the bounded tracked_ttl_clamped configuration signal
dialcache_invalidation_counterCountercache_namespace, key_type, layerInvalidation calls for the layers touched
dialcache_coalesced_counterCountercache_namespace, use_case, key_type, scopeCoalesced requests split by request_local or process scope
dialcache_shadow_validation_counterCountercache_namespace, use_case, key_type, outcomeSampled Redis shadow-job outcomes
dialcache_shadow_value_age_histogramHistogramcache_namespace, use_case, key_type, outcomeAge in seconds of the validated Redis value at shadow verdict time, recorded for match and mismatch
dialcache_future_timestamp_offset_histogramHistogramcache_namespace, use_case, key_type, layerPositive offset in seconds for a valid frame dated after the observing process clock
dialcache_stale_recovery_counterCountercache_namespace, use_case, key_type, outcomeClassifier-authorized stale-recovery checks: served, miss, or deserialization_error
dialcache_stale_recovery_value_age_histogramHistogramcache_namespace, use_case, key_type, outcomeActual return-time age in seconds of a retained value, recorded only for served
dialcache_compression_counterCountercache_namespace, use_case, key_type, layer, outcomePayload compression outcomes: writes record compressed, below_threshold, not_smaller, or write_over_limit; reads record decompressed, fallback_raw, or read_over_limit
dialcache_get_timerHistogramcache_namespace, use_case, key_type, layerCache get latency in seconds
dialcache_fallback_timerHistogramcache_namespace, use_case, key_type, layerElapsed time until the underlying function settles or timeout rejection is delivered
dialcache_serialization_timerHistogramcache_namespace, use_case, key_type, layer, operationRedis serializer dump/load latency
dialcache_size_histogramHistogramcache_namespace, use_case, key_type, layerSerialized Redis payload size in bytes, before compression
dialcache_stored_size_histogramHistogramcache_namespace, use_case, key_type, layerPrepared Redis payload size in bytes, after compression and escaping; before dispatch
dialcache_compression_ratio_histogramHistogramcache_namespace, use_case, key_type, layerCompressed-to-original payload size ratio for compressed writes
dialcache_compression_timerHistogramcache_namespace, use_case, key_type, layer, operationPayload compression and decompression latency in seconds

policy_disabled means that a process-local or remote layer has no effective TTL after runtime overlays. This is an intentional policy result, including the default when defaultConfig is omitted, rather than a configuration-loading failure.

Every metric includes cache_namespace, even disabled-context, key-construction, coalescing, and invalidation paths that do not have a constructed key. Its value is DialCacheConfig.namespace, which defaults to urn.

The layer label is:

  • request_local;
  • local, meaning process-local;
  • remote;
  • remote_shadow for Redis reads, fills, serialization, compression, and payload sizes performed by detached shadow jobs; or
  • noop for disabled-context, key-construction, and config-provider failures where no cache layer was reached.

The bounded scope label on dialcache_coalesced_counter distinguishes request_local from process. scope="process" coordinates calls only within one DialCache instance; separate instances in the same process do not share in-flight state. A use case with coalesce: false emits no coalesced counter; each caller instead emits its own request, miss, duration, and error metrics.

Shadow outcomes

dialcache_shadow_validation_counter reports one terminal outcome for each admitted or explicitly dropped shadow job. Datadog exposes the same bounded outcomes through dialcache.shadow.count:

outcomeMeaning
matchThe cached and source values matched.
mismatchThey differed, and a confirmation read found the original Redis payload unchanged.
supersededThey differed, but the Redis payload changed or disappeared before confirmation.
filledA clean shadow miss was populated successfully.
fill_fencedA timestamp check skipped tracked fill dispatch against the watermark observed in the initial read.
fill_errorPreparing the payload (serialization or compression) or writing a clean-miss fill failed.
redis_errorThe initial detached Redis read failed.
source_errorThe source-of-truth read failed.
deserialization_errorThe retained Redis payload could not be deserialized for comparison.
comparison_errorThe comparator threw or did not return a synchronous boolean.
confirmation_errorThe confirmation Redis read failed.
timeoutThe shadow deadline expired.
droppedPer-key deduplication or the instance flight cap rejected the job.

The outcome counter deliberately has no layer or cache-id label. Operational Redis metrics produced inside the same job use layer="remote_shadow", which keeps detached work separate from caller-serving layer="remote" telemetry. See Shadow validation for the read, confirmation, fill, and deadline semantics behind these outcomes.

Stale recovery outcomes

dialcache_stale_recovery_counter and dialcache.stale_recovery.count record one outcome for each classifier-authorized recovery check:

OutcomeMeaning
servedA retained candidate passed return-time age checks and supplied the result
missNo candidate remained eligible
deserialization_errorDecoding the retained candidate failed

Only served emits the corresponding value-age observation. Recovery adds no second Redis request, miss, or read-duration sequence. The source failure and fallback duration remain visible even when a snapshot serves. Classifier denial emits no recovery outcome. See Stale-on-error.

Value ages and clock offsets

Shadow value age is reported only for match and confirmed mismatch, at verdict time. Recovery age is reported only for served, at return time. Both use the observing application's epoch clock minus the frame's writer timestamp. Shadow age uses the original C0 timestamp, even if confirmation finds identical payload bytes with a newer timestamp. It clamps to zero after clock rollback and skips nonfinite age observations.

The future-offset histogram records a positive offset for valid decoded frames ahead of the observer clock. Ordinary and initial-shadow reads then miss; confirmation can retain the frame only for comparison. Invalid timestamps never enter histogram sums. Repeated reads can observe the same future frame.

For direct adapter callers, Prometheus additionally discards nonfinite or nonpositive observeFutureTimestampOffset values. Datadog forwards those observations without that extra guard; normal DialCache calls supply positive finite offsets to both.

Use external fleet clock monitoring as well: workload observations cannot detect every skew direction or determine which node is wrong. Its dedicated histogram buckets cover millisecond-scale through multi-hour faults.

Compression metrics

Compression telemetry is bounded and uses layer="remote" for caller-serving work or layer="remote_shadow" for detached shadow work.

Write-side outcomes are:

  • compressed: zstd plus its envelope was smaller and selected for the prepared Redis payload;
  • below_threshold: the serialized payload did not reach the configured threshold; this check runs before the size ceiling;
  • not_smaller: compression ran, but the marked result was not smaller than the raw stored form; and
  • write_over_limit: the serialized value reached the threshold but exceeded the 512 MiB decompression ceiling and was kept raw for the attempted write. This is a capacity signal, not an error.

Read-side outcomes are:

  • decompressed: a marked zstd payload was restored;
  • fallback_raw: native zstd rejected a marked payload for a reason other than the output limit, so it was passed unchanged to the serializer; and
  • read_over_limit: decompression would exceed the 512 MiB ceiling, so the stored bytes were passed unchanged to the serializer. Treat this as a corruption or integrity signal.

Raw reads do not emit a compression outcome. With compression: false, new writes are still escaped when necessary but emit no compression outcome; reads continue to report marked values because disabling writes does not disable decoding.

dialcache_size_histogram measures serializer output before compression and is the distribution to use when selecting thresholdBytes. dialcache_stored_size_histogram measures the prepared bytes after compression or binary-envelope escaping. DialCache records it before final shadow-deadline/fence gates and before calling the Redis client, so it is not proof that a write was dispatched or succeeded. The ratio histogram is emitted when compression selects the smaller representation, at the same pre-write stage.

Compression duration is observed when zstd runs and produces either compressed or not_smaller; decompression duration is observed for each marked payload that produces a read-side outcome.

A zstd exception while preparing a write records error="compression" and the cache write fails open. Decompression rejects neither the cache call nor the observer path directly: rejected marked bytes reach the configured serializer. If load rejects, it records serialization_load. An ordinary fresh read becomes a refreshable miss; shadow comparison reports deserialization_error without repair, and retained recovery preserves the original source rejection. See Serialization.

zstd work is synchronous on the Node.js event loop. Use the duration, ratio, and pre/post-size series together when changing the threshold or level; a good space ratio does not make an event-loop stall acceptable. See Redis payload compression for the envelope, limits, and mixed-version rollout contract.

Confirmed mismatch warnings

Shadow metrics remain bounded and contain no cache ids or values. A use case can separately set shadow.logMismatches: true to emit one warning after a terminal mismatch is confirmed. Logging is default-off, does not replace the outcome metric, and does not activate shadow work without the shadowValidation hook.

The warning contains stable metadata, the logical cache key capped at 2 KiB, and independently generated native-JSON strings for the cached and source comparator inputs capped at 8 KiB each. Those fields are value-bearing, and truncation is not redaction.

See Confirmed mismatch logging for confirmation semantics, exact fields, JSON behavior, operational limits, and data-handling considerations.

Error categories

The error label reports the operation that failed instead of copying the thrown value's class or Error.name:

errorMeaning
key_constructionThe cache-key selector or DialCacheKey construction failed
config_resolutionRuntime or layer configuration validation or resolution failed
cache_readA process-local read or non-timeout remote read failed
cache_read_timeoutA remote read exceeded its effective DialCache deadline
cache_writeA process-local or remote cache write failed
tracked_ttl_clampedA dispatched tracked write requested retention above the one-hour physical cap
serialization_loadDeserializing a Redis payload failed
serialization_dumpSerializing a value for Redis failed
compressionzstd compression failed while preparing a Redis write
invalidationWriting an invalidation watermark failed
fallbackThe source loader failed or exceeded its DialCache deadline
unknownReserved for a future failure site that cannot be classified otherwise

DialCache defines these values itself, so they are identical for every adapter.

A valid invalidateRemote() call without a configured Redis client is still an invalidation attempt: DialCache records dialcache_invalidation_counter (or dialcache.invalidation.count), logs the failure, records error="invalidation", and rejects with the original focused TypeError. Invalid futureBufferMs input is rejected before these observers run.

Caller-serving remote-read timeouts use layer="remote" and in_fallback="false". They are errors rather than misses, and the remote get-duration observation includes the wait. Coalesced followers do not multiply the timeout error. Deadline details remain out of labels and are available on the logged RedisReadTimeoutError.

Detached initial and confirmation reads attribute their operational metrics to remote_shadow. Read failures, including read timeouts, can report redis_error or confirmation_error without a matching error log. The overall shadow deadline instead reports timeout; see Shadow outcomes.

Raw thrown values, error names, messages, cache ids, arguments, and Redis keys are never included in labels. When DialCache logs a cache-plumbing failure, the raw details remain available through the configured logger; not every metric error or shadow outcome has a matching log entry.

The explicitly opted-in confirmed-mismatch warning is a separate value-bearing log and does not alter the metric schema.

in_fallback remains the explicit distinction between cache plumbing and application fallback failures.

Custom adapters

Implement DialCacheMetricsAdapter and pass it through new DialCache({ metrics }) for another telemetry backend.

HookRequiredValue
request(labels)yesOne active cache-layer lookup.
miss(labels)yesOne cache miss with required bounded reason (MissMetricLabels).
disabled(labels)yesOne skipped layer or no-layer invocation with a bounded reason.
error(labels)yesOne bounded failure site with inFallback.
invalidation(labels)yesOne explicit remote invalidation call.
coalesced(labels)noOne follower that joined request-local or process-scoped work.
shadowValidation(labels)noOne terminal sampled-shadow outcome. This hook must be implemented for shadow jobs to execute.
observeShadowValueAge(labels, seconds)noAge for shadow match/mismatch verdicts; does not gate admission.
observeFutureTimestampOffset(labels, seconds)noPositive decoded-frame clock offset; does not change read decisions.
staleRecovery(labels)noOne authorized recovery outcome; omission does not disable recovery.
observeStaleRecoveryValueAge(labels, seconds)noReturn-time age for served recovery only.
compression(labels)noOne bounded compression or decompression outcome.
observeGet(labels, seconds)yesCache-read duration in seconds.
observeFallback(labels, seconds)yesFallback duration in seconds.
observeSerialization(labels, seconds)yesSerializer dump/load duration in seconds.
observeSize(labels, bytes)yesSerialized remote payload size in bytes, before compression.
observeStoredSize(labels, bytes)noPrepared remote payload size in bytes, after compression and escaping; emitted before client dispatch.
observeCompressionRatio(labels, ratio)noCompressed-to-original size ratio when compression selects the prepared representation.
observeCompression(labels, seconds)noCompression or decompression duration with operation="compress" or operation="decompress".

The root package exports DialCacheMetricsAdapter and every associated label, reason, error-kind, layer, scope, and shadow-outcome type, including ShadowValidationMetricLabels, ShadowValidationOutcome, CompressionMetricLabels, CompressionOperationMetricLabels, and CompressionOutcome, CacheMissReason, MissMetricLabels, StaleRecoveryMetricLabels, and StaleRecoveryOutcome. shadowValidation remains optional so existing custom adapters keep compiling, but DialCache does not admit shadow work when the configured adapter omits it. The Prometheus and Datadog adapters implement the hook.

The compression hooks are also optional for source compatibility with existing custom adapters. They control observation only: omitting them does not disable compression or decompression. The Prometheus and Datadog adapters implement all four hooks.

Metrics and logger methods are typed void and invoked as fire-and-forget observers. DialCache also defensively consumes, but never awaits, a thenable returned at runtime.

Synchronous throws and rejections of those returned thenables are isolated so telemetry cannot change cache correctness, fallback results, or shadow outcomes. This guard applies when DialCache invokes the observer, not to direct calls to an adapter or to asynchronous work whose promise the hook does not return.

A custom adapter may buffer or transmit asynchronously, but it owns delivery, flushing, resources, and shutdown after the call returns. Keep application-owned namespace, use-case, and key-type labels stable and low-cardinality, and preserve the seconds and bytes units shown above.

Every backend-neutral label object exposes the logical namespace as camel-case cacheNamespace. Map it to the backend's cache_namespace label or tag. This field is present even when no key or cache layer was reached.

Omit metrics to disable metrics entirely. Because shadow jobs require an observable terminal outcome, omitting metrics also disables shadow execution even when a key policy sets shadow.ramp or enables shadow.logMismatches.

See Metric migrations when upgrading collectors, miss queries, or exhaustive outcome mappings.