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.
| Kind | Accepted | Anything else |
|---|---|---|
| String | Any non-empty value | An empty string counts as unset, so the default applies |
| Boolean | 1, or true in any casing | Any other non-empty value is false |
| Integer | Parsed base 10 | A 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
| Variable | Default | Effect |
|---|---|---|
REDIS_URL | unset | Redis URL when setupCache has no explicit client or URL. Ignored with redis: false |
LAZY_LAYERS_NAMESPACE | npm_package_name, then app | Namespace when no explicit namespace is passed |
npm_package_name | app | Package-name fallback for the managed namespace |
Use an explicit namespace for instances that must share a cache. See setup options.
Serialization
| Variable | Accepted values | Default | Effect |
|---|---|---|---|
CACHE_COMPRESSION | Exactly none, gzip, zstd, auto, or a JSON tier array | Size tiers equivalent to auto | Process-wide compression default |
CACHE_FORMAT | Exactly json | unset | Selects tagged JSON when the write has no explicit format |
CACHE_DEBUG_SERIALIZATION | Exactly true | unset | Same JSON switch |
CACHE_COMPRESSION
| Value | Tiers |
|---|---|
auto | None below 256 B, LZ4 below 4 KiB, then Zstd when available or LZ4 otherwise |
none | No compression |
gzip | None below 1 KiB, then gzip |
zstd | None 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
| Variable | Accepted values | Default | Effect |
|---|---|---|---|
NODE_ENV | Exactly production selects production | unset | Anything 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.
| Variable | Kind | Default | Effect |
|---|---|---|---|
LAZY_OBS_ENABLED | boolean | false | Master switch for the dashboard |
LAZY_OBS_ROUTE | string | /__lazylayers | Base route, normalised to a leading slash with no trailing slash |
LAZY_OBS_HOST | string | 127.0.0.1 | Interface the standalone server binds |
LAZY_OBS_PORT | integer | 7077 | Port the standalone server listens on |
LAZY_OBS_NO_SERVER | boolean | false | Skip the standalone server and expose only the mountable handler |
LAZY_OBS_USER | string | lazydev | Basic-auth username |
LAZY_OBS_PASSWORD | string | lazydev | Basic-auth password |
LAZY_OBS_TOKEN | string | unset | Shared token, accepted as ?token= or as a bearer header |
LAZY_OBS_NO_AUTH | boolean | false | Remove authentication entirely |
LAZY_OBS_MAX_EVENTS | integer | 1000 | Size of the in-memory ring buffer for the live feed |
LAZY_OBS_MAX_VALUE_BYTES | integer | 262144 | Decoded values larger than this are truncated in the UI. 256 KB |
LAZY_OBS_PROMETHEUS | boolean | false | Expose an exposition endpoint at {route}/metrics |
LAZY_OBS_PROMETHEUS_PREFIX | string | lazycache | Metric name prefix |
LAZY_OBS_PROMETHEUS_PUBLIC | boolean | false | Allow unauthenticated scrapes of the metrics endpoint while the UI stays behind auth |
LAZY_OBS_QUIET | boolean | false | Suppress 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
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.
| Variable | You pass it to |
|---|---|
REDIS_URL | Your ioredis client constructor |
RABBITMQ_URL | RabbitMQEventBus as url |
NATS_URL | NatsEventBus as connectionOptions.servers |
INSTANCE_ID | The 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
Observability guide
What the dashboard shows, how to mount it in your own server, and how to secure it properly.
Configuration reference
The constructor options each of these variables is a fallback for.
Production checklist
Where INSTANCE_ID comes from under clustering, and what else has to be unique per server.