LazyLayersv0.5.3
Reference

Environment variables

Every variable the library reads, the exact strings it accepts, and what happens when you leave it unset.

Environment variables configure managed setup, process-wide serialization and logging, and the observability dashboard.

For every observability variable the order is: an explicit constructor option wins, then the environment variable, then the built-in default. One variable breaks that rule, and it is called out below.

How observability values are parsed

The parsing is strict. A variable that does not match is treated as unset, or as false, with no warning.

KindAcceptedAnything else
StringAny non-empty valueAn empty string counts as unset, so the default applies
Boolean1, or true in any casingAny other non-empty value is false
IntegerParsed base 10A value that does not parse to a finite number counts as unset

So LAZY_OBS_ENABLED=yes does not enable anything, and LAZY_OBS_ENABLED=TRUE does.

Managed setup

VariableDefaultEffect
REDIS_URLunsetRedis URL when setupCache has no explicit client or URL. Ignored with redis: false
LAZY_LAYERS_NAMESPACEnpm_package_name, then appNamespace when no explicit namespace is passed
npm_package_nameappPackage-name fallback for the managed namespace

Use an explicit namespace for instances that must share a cache. See setup options.

Serialization

VariableAccepted valuesDefaultEffect
CACHE_COMPRESSIONExactly none, gzip, zstd, auto, or a JSON tier arraySize tiers equivalent to autoProcess-wide compression default
CACHE_FORMATExactly jsonunsetSelects tagged JSON when the write has no explicit format
CACHE_DEBUG_SERIALIZATIONExactly trueunsetSame JSON switch

CACHE_COMPRESSION

ValueTiers
autoNone below 256 B, LZ4 below 4 KiB, then Zstd when available or LZ4 otherwise
noneNo compression
gzipNone below 1 KiB, then gzip
zstdNone below 1 KiB, then Zstd, falling back to LZ4 when unavailable

The variable is read once when the serializer is imported. A later configureCompression call replaces the process default. Explicit levels.L1.codec.compression and levels.L2.codec.compression override it for their layer.

For custom size bands:

CACHE_COMPRESSION='[{"maxBytes":256,"codec":"none"},{"maxBytes":4096,"codec":"lz4"},{"codec":"zstd"}]'

Malformed JSON or an invalid tier array is ignored. Log getCompressionTiers() at startup to inspect the active process default. See serialization configuration for tier validation rules.

The JSON debug switches

CACHE_FORMAT=json and CACHE_DEBUG_SERIALIZATION=true each select uncompressed JSON for writes without an explicit format. These comparisons are case-sensitive. CACHE_FORMAT=JSON and CACHE_DEBUG_SERIALIZATION=1 do not enable JSON.

A layer's explicit format: 'msgpack' overrides these switches. Explicit format: 'json' selects JSON without environment variables. JSON follows JSON value semantics, so Date, Map, and typed arrays do not retain their JavaScript types.

Reads continue to use the stored tag. Changing a write policy does not rewrite existing values, and readers need the codec required by each stored value.

Logging

VariableAccepted valuesDefaultEffect
NODE_ENVExactly production selects productionunsetAnything other than production prints debug lines on every hit, miss, set and delete

Two things about this one.

It is read once, when the logging module is first imported. Mutating process.env.NODE_ENV after your application has loaded changes nothing.

It is a default, not a floor. logging.env overrides it, and logging.enabled overrides both. That configuration is process-wide and applied by every cache and every event bus you construct, so the last one constructed wins for the whole process.

Observability

Every dashboard setting has a variable, so you can turn the dashboard on in staging without shipping a code change.

These are resolved in the cache constructor, so they have to be set before you construct the cache. Changing one at runtime does nothing.

VariableKindDefaultEffect
LAZY_OBS_ENABLEDbooleanfalseMaster switch for the dashboard
LAZY_OBS_ROUTEstring/__lazylayersBase route, normalised to a leading slash with no trailing slash
LAZY_OBS_HOSTstring127.0.0.1Interface the standalone server binds
LAZY_OBS_PORTinteger7077Port the standalone server listens on
LAZY_OBS_NO_SERVERbooleanfalseSkip the standalone server and expose only the mountable handler
LAZY_OBS_USERstringlazydevBasic-auth username
LAZY_OBS_PASSWORDstringlazydevBasic-auth password
LAZY_OBS_TOKENstringunsetShared token, accepted as ?token= or as a bearer header
LAZY_OBS_NO_AUTHbooleanfalseRemove authentication entirely
LAZY_OBS_MAX_EVENTSinteger1000Size of the in-memory ring buffer for the live feed
LAZY_OBS_MAX_VALUE_BYTESinteger262144Decoded values larger than this are truncated in the UI. 256 KB
LAZY_OBS_PROMETHEUSbooleanfalseExpose an exposition endpoint at {route}/metrics
LAZY_OBS_PROMETHEUS_PREFIXstringlazycacheMetric name prefix
LAZY_OBS_PROMETHEUS_PUBLICbooleanfalseAllow unauthenticated scrapes of the metrics endpoint while the UI stays behind auth
LAZY_OBS_QUIETbooleanfalseSuppress the one-time startup notice

The username variable is LAZY_OBS_USER, not LAZY_OBS_USERNAME. The constructor option for the same value is observability.auth.username, so the two names differ on purpose. LAZY_OBS_USERNAME is read by nothing and will leave you on the default credentials while looking like you changed them.

The one exception to precedence

LAZY_OBS_NO_SERVER is not overridden by an option. The standalone server is skipped when observability.server === false or the variable is true, so setting LAZY_OBS_NO_SERVER=true disables a server your code asked for.

The reverse does not hold. LAZY_OBS_NO_SERVER=false cannot bring back a server that server: false turned off, because only the value true participates.

Settings with no variable

observability.server.autoStart has no environment equivalent. Neither does observability.enabled at the level of a single cache instance: LAZY_OBS_ENABLED applies to every cache in the process that does not set enabled explicitly.

Precedence in practice

src/cache/index.js
import { LazyLayersCache, RedisStore } from 'lazy-layers-cache'
import { redis } from './redis.js'

export const cache = new LazyLayersCache({
  ttlMs: 5 * 60 * 1000,
  l2: new RedisStore(redis, { prefix: 'app:cache:' }),

  observability: {
    // Left to LAZY_OBS_ENABLED, so staging can turn this on without a deploy.
    // Pinned in code, so no environment can move the dashboard somewhere
    // your reverse proxy is not expecting.
    route: '/internal/cache',
    // Credentials stay in the environment. Never in the repository.
  },
})

Anything you set in code is fixed for every environment. Anything you leave out is yours to move per environment. Credentials belong in the second group.

The shipped defaults are lazydev and lazydev, on localhost. They are development credentials and nothing more. Set LAZY_OBS_USER and LAZY_OBS_PASSWORD before the dashboard is reachable by anything other than the host it runs on, and treat LAZY_OBS_HOST as the moment you have made it reachable.

LAZY_OBS_NO_AUTH=true removes authentication completely and publishes cached values to anyone who can reach the port. It has no place in any environment that is not your own machine.

Variables the library does not read

These appear throughout the documentation because they are the conventional place to keep connection details. Nothing in the library looks them up. You pass them yourself.

VariableYou pass it to
REDIS_URLYour ioredis client constructor
RABBITMQ_URLRabbitMQEventBus as url
NATS_URLNatsEventBus as connectionOptions.servers
INSTANCE_IDThe cache as source, and the bus as a durable queue or consumer name

INSTANCE_ID is the one worth wiring carefully, because three separate things break when two servers share a value.

As source, it is how a server recognises its own broadcasts. Two servers sharing it means each ignores the other's invalidations, so deletes silently stop propagating between them.

As a RabbitMQ queueName in durable mode, it names the queue that server consumes from. Two servers sharing it means they consume from one queue and each invalidation reaches only one of them.

As a NATS JetStream durableName, the same thing happens through a shared durable consumer: the stream is split between the servers rather than delivered to both.

Under Node clustering, every worker is a separate server for all three purposes. Derive the value from the worker id, not just the host.

Where to next

On this page