Update environment variables without restarting your Node app

Short answer: a running process gets a copy of its environment when it starts, and nothing outside can edit that copy. To change a value without redeploying, either restart the process with a new environment (fast if something supervises it), or give the app a way to read values while it runs and rebuild whatever was built from them. The second option is what "hot reload" means for secrets, and the demo below shows it with a database pool.

Developers have been asking this for years. A Stack Overflow question titled “How to update ENV variables in a Process without restarting it (NodeJS)?” has 38,453 views (read 2026-11-01) and its three highest-voted answers score 4, 4 and 2, none accepted. A sibling, “reload process.env variables without reboot”, has 6,486. This page gives the straight answer, then a runnable example.

Why process.env can’t be changed from outside

When the operating system starts a program it hands it an environment block. In Node, process.env is how your code sees that block. Your own code can assign to it, but no other process can reach in and change it, and a file you edit later (a .env, a config map) is only read when something reads it. If you have ever edited .env, saved, and wondered why nothing changed, that is the reason. Libraries like dotenv load a file once at startup unless you call them again.

Note what this question is not: it is not the dev-time “hot reload” of nodemon or Vite, which restart or patch your code when files change. This is about production values (a proxy password, an API key, a database URL) changing while the service keeps serving.

Four ways to get new values into a running service

Approach How it works Good for Watch out for
Restart with the new env A supervisor (systemd, Docker, dynv run) stops the process and starts it with the new environment Anything: startup flags, ports, NODE_OPTIONS Brief unavailability unless you roll; connections drop
Re-read a file on a timer or watcher Your code watches .env or a mounted secret and reloads it Simple, no extra service You rebuild clients yourself; partial writes; Kubernetes refresh lag is a platform detail
Poll a secrets API Your code asks a service for the latest value every N seconds Works anywhere Your code holds credentials for that API; polling delay; each language needs a client
Push from a local agent, rebuild on change An agent next to the app fetches, verifies and decrypts; your code gets change events and rebuilds what depends on them Pools, API clients, proxy agents that must change live Needs the agent or SDK; you decide what is safe to rebuild

The rest of the article uses the last row with Dynv, because that is what we built and can show working. The restart row is also built in: a variable defaults to restart mode, and dynv run restarts your process when it changes.

Demo: swap a database pool live

This was run against a local Dynv server and the current CLI and SDK on 2026-11-01; the output below is the real output. You need the CLI (quickstart, steps 0 to 3) and the Node SDK. The SDK is in the repository at sdk/ts; the npm package is not published yet, so install it from a checkout (SDK docs).

1. Two variables on a service named web. One will be hot, one stays in restart mode. (Real values belong in dynv admin set web --stdin, which reads a dotenv document so they stay out of your shell history; the demo values are fake.)

dynv admin set web DATABASE_URL="postgres://app:[email protected]:5432/app" LOG_LEVEL=info
dynv admin mode web hot DATABASE_URL

2. The app. resource() builds a long-lived object from variables and rebuilds it when they change, letting in-flight work finish on the old one. We use a stand-in class so the demo needs no database; swap in pg.Pool or your driver.

// app.mjs
import { dynv } from "@dynv/sdk";

const d = dynv();
await d.ready();

class Pool {
  constructor(url) { this.url = url; console.log(`pool opened -> ${new URL(url).host}`); }
  close() { console.log(`pool closed -> ${new URL(this.url).host}`); }
}

const pool = d.resource(["DATABASE_URL"], (env) => new Pool(env.DATABASE_URL), {
  dispose: (old) => old.close(),
  drainTimeoutMs: 5_000,
});

console.log(`pid ${process.pid}, LOG_LEVEL=${process.env.LOG_LEVEL}`);
setInterval(() => pool.use((p) => console.log(`pid ${process.pid} query via ${new URL(p.url).host}`)), 1000);

3. Run it under dynv run with a token for the service:

export DYNV_URL=https://api.dynv.dev DYNV_PROCESS=web
export DYNV_TOKEN=dynv_pt_...        # a token for the web service
dynv run -- node app.mjs

4. Change the hot variable from another terminal:

dynv admin set web DATABASE_URL="postgres://app:[email protected]:5432/app"

The app’s output, same process id throughout:

pool opened -> db-one.internal:5432
pid 1204611, LOG_LEVEL=info
pid 1204611 query via db-one.internal:5432
pid 1204611 query via db-one.internal:5432
dynv: v5 applied; changed: [DATABASE_URL] (hot)
pool opened -> db-two.internal:5432
pool closed -> db-one.internal:5432
pid 1204611 query via db-two.internal:5432

The new pool was built first, new queries went to it, and the old one was closed after its in-flight work drained.

5. Change the restart-mode variable:

dynv admin set web LOG_LEVEL=debug
dynv: v6 applied; changed: [LOG_LEVEL] -> restarting
pool opened -> db-two.internal:5432
pid 1206323, LOG_LEVEL=debug

A different pid: LOG_LEVEL was read from process.env at startup, so only a restart can change it. dynv run did that (SIGTERM, then SIGKILL after 10 s if needed) with the new environment, and the app came back on the latest DATABASE_URL.

Choosing what to make hot

  • Hot (rebuild on change): things you construct from a value and can reconstruct: database pools, HTTP clients, proxy agents, API keys passed per request.
  • Restart (the default): anything read once at startup or by the runtime: ports, NODE_OPTIONS, framework config, feature flags read into constants. Variables that control how the runtime starts or loads code (NODE_OPTIONS, PATH) cannot change under a running process, so keep them in restart mode.
  • Don’t cache a hot value in a module-level constant: it pins the old one. Call d.get("KEY") at the point of use, or build through resource().

What happens when things go wrong

  • Your factory throws on the new value: the old instance stays current and an error event fires, so a typo doesn’t take the service down (resource docs).
  • Dynv is unreachable: a running service keeps its last good values. For restarts while Dynv is down, start dynv run with a cache (--cache or DYNV_CACHE); it holds ciphertext only. Without one, startup fails by default (concepts).
  • You want it back: set the previous value; it is another change event.
  • Rotating a database password: both credentials must be valid at the database during the swap, since in-flight work finishes on the old pool. Create the new role or password first, switch, then drop the old one.

When this is not the answer

  • Serverless or platforms where you can’t wrap the start command: the SDK has a direct mode that holds a service token in the app’s process; read the trade-offs in the SDK docs first. Python and Go SDKs exist too.
  • You already have an orchestrator that restarts safely: a rolling restart may be all you need. Hot reload shines for connection-heavy services where a restart is visible.
  • Per-request values that change constantly (feature flags with targeting): that is a different system.

Values travel encrypted, and Dynv’s servers hold only ciphertext, so the pattern doesn’t mean handing your secrets to a third party in plaintext. The cost is that the agent or SDK has to be part of your deploy.

Facts last verified . Spotted an error? Tell us.

New here? Start here for the guides in a sensible order.