features·2026-07-01·3 min read

Reactive Environment Variables with watchEnv

Long-running processes can now react to environment variable changes at runtime — no restart required.

Ctrotech·
featureguide
RSS Feed

Reactive Environment Variables with watchEnv

Environment variables have always been a "set once at startup" affair — change a value and you restart the process. watchEnv() breaks that pattern for long-running applications.

The Problem

Servers, CLI watchers, and development tooling all suffer from the same friction: you tweak a .env value and have to restart the whole process to pick up the change.

// Traditional: one-shot at process start
const env = defineEnv({ PORT: number().port() })
// PORT is now frozen — any change requires a restart

During development, this means killing and restarting a dev server. In production, it means a rolling deploy or a SIGHUP handler.

Watching for Changes

watchEnv() takes the same schema as defineEnv() but polls the source for changes:

import { watchEnv, string, number } from "@ctroenv/core"

const env = watchEnv(
  { DATABASE_URL: string().url(), PORT: number().port().default(3000) },
  { pollInterval: 2000, onChange: (key, old, next) => console.log(`${key}: ${old} -> ${next}`) },
)

The env object works exactly like a normal defineEnv() result — TypeScript types, secret masking, the env.meta API — but values update automatically when the source changes.

Callbacks

The onChange callback fires with the key, old value, and new value every time a variable changes:

watchEnv(schema, {
  onChange(key, oldVal, newVal) {
    log.info(`Env var "${key}" changed`, { old: oldVal, new: newVal })
    if (key === "LOG_LEVEL") reconfigureLogger(newVal)
  },
})

Use this to react to changes — reconfigure loggers, update connection pools, or invalidate caches — without restarting.

Error Handling

If the source changes to values that fail validation, watchEnv doesn't crash:

watchEnv(schema, {
  onError(errors) {
    console.error("Validation failed on change:", formatErrors(errors))
  },
})

The previous valid values remain in place. The env object stays consistent until the source produces valid values again.

Cleanup

Every watchEnv() returns an unwatch() function:

const env = watchEnv(schema)
// Later, when shutting down or no longer needing updates:
env.unwatch()
clearInterval(env.unwatch) // internal timer is cleaned up

This prevents stale watches in long-lived modules, test suites, or hot-reload scenarios.

AbortSignal Support

For frameworks that use AbortController, pass a signal:

const controller = new AbortController()

const env = watchEnv(schema, { signal: controller.signal })
// On shutdown:
controller.abort()

When to Use watchEnv

Good candidates:

  • Dev servers — restart-less .env edits
  • CLI watchers — ctroenv validate --watch is built on this
  • Long-running workers — reconfigure without restart
  • Testing — simulate env changes without spawning new processes

Stick with defineEnv() for serverless functions, build-time validation, and one-shot scripts.

Full Changelog

See the GitHub Release for the complete list of changes.

More features posts
Next postBuild-Time vs Runtime Validation

On this page

The ProblemWatching for ChangesCallbacksError HandlingCleanupAbortSignal SupportWhen to Use watchEnvFull Changelog