{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreif42tkvxxigwb4rrwf22vfjev4kau3gb5pl3ktj2d6srioascynsa",
"uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3movu5ubrxxm2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreide7rzrflhffqlsvfnfcsh5gefyurb3xbn6levzlbu43lfcpxnymi"
},
"mimeType": "image/webp",
"size": 323662
},
"path": "/ctrotech/10-things-nobody-tells-you-about-processenv-1o5b",
"publishedAt": "2026-06-22T21:15:33.000Z",
"site": "https://dev.to",
"tags": [
"typescript",
"node",
"javascript",
"webdev",
"GitHub",
"Docs",
"@ctroenv"
],
"textContent": "## 10 Things Nobody Tells You About process.env\n\nI've burned myself on most of these so you don't have to. Here's what I wish someone had told me early on.\n\n## 1. Keys are case-sensitive on Linux, case-insensitive on Windows\n\n\n process.env.PORT = \"3000\"\n console.log(process.env.port) // undefined on Linux, \"3000\" on Windows\n\n\nThis one got me during a \"works on my machine\" incident. My Windows dev box ran fine. The Linux CI server crashed because a teammate typed `env.port` instead of `env.PORT`. Your CI runs Linux. Your dev box probably runs macOS or Windows. Case-sensitivity differences will bite you.\n\n**How to handle it** : Use a validation layer that throws on missing keys. A simple `getEnv(\"PORT\")` will catch typos at startup.\n\n## 2. Values are always strings\n\n\n console.log(typeof process.env.PORT) // \"string\" even if you set PORT=3000\n\n\n`Number(process.env.PORT)` can return `NaN` without throwing. Boolean values like `\"false\"` are truthy strings.\n\n**How to handle it** : Always parse. If you use a schema library like CtroEnv, it coerces types and throws on invalid input.\n\n## 3. process.env is NOT the same as .env\n\nThis confused me for way too long. `process.env` is whatever the shell gave the process. A `.env` file is just a text file dotenv reads to populate `process.env`. Node doesn't touch `.env` files on its own.\n\n\n\n // This won't read .env automatically\n console.log(process.env.MY_VAR) // undefined\n\n\n**How to handle it** : Call `dotenv.config()` at entry, or use `@ctroenv/node` which loads `.env` files automatically.\n\n## 4. You can set env vars per-command\n\n\n PORT=4000 node app.js\n\n\nThis sets `PORT` only for that single process. It doesn't pollute your shell session. Super useful for one-off runs or testing different configurations without editing files.\n\n\n\n console.log(process.env.PORT) // \"4000\"\n\n\n## 5. process.env is mutable at runtime\n\n\n process.env.DATABASE_URL = \"postgres://hacker:gotme@evil.com/db\"\n\n\nI've seen code that modifies `process.env` to \"fix\" config at runtime. Don't do this. If something is wrong, fail fast and fix the source. Mutating `process.env` makes debugging a nightmare — you can't trust what you see anymore.\n\n**If you're using CtroEnv** , the returned object is frozen. You literally can't mutate it.\n\n## 6. Next.js inlines NEXT_PUBLIC_ vars at build time\n\n\n // This gets replaced at BUILD time, not runtime\n console.log(process.env.NEXT_PUBLIC_API_URL)\n\n\nNext.js replaces `process.env.NEXT_PUBLIC_*` references with their actual values during `next build`. After that, changing the env var on the server does nothing. You have to rebuild.\n\n**What this means** : If you change `NEXT_PUBLIC_API_URL` on your production server, your app still uses the old value. Found this out the hard way during a hotfix.\n\n## 7. process.env is not available in the browser\n\nBrowsers don't have `process`. If you're using Webpack or Vite, they emulate `process.env` at build time for variables you specifically expose.\n\n\n\n // In the browser with Vite:\n console.log(import.meta.env.VITE_API_URL) // works\n console.log(process.env.PORT) // ReferenceError\n\n\n**How to handle it** : Use Vite's `import.meta.env` with the `VITE_` prefix, or reach for a framework adapter that handles this split automatically.\n\n## 8. NODE_ENV is not set by default\n\nI used to assume `NODE_ENV` was always there. It's not. Node.js doesn't set it. Your framework probably does — Express sets it to \"development\" by default, Next.js sets it during build — but if you're writing a bare Node script, you need to set it yourself.\n\n\n\n if (process.env.NODE_ENV === \"production\") {\n // This might never run if you forgot to set it\n }\n\n\n**How to handle it** : Always provide a default. Or validate it exists if your app can't run without it.\n\n## 9. Env var values are limited to ~32KB on some systems\n\nThis one's rare but brutal when it hits. Some Unix systems cap a single environment variable value at 32KB (or even smaller on older kernels). Windows has a 32,767 character limit per variable. If you're stuffing a PEM-encoded certificate or a large JSON blob into an env var, you might hit invisible truncation.\n\n**How to handle it** : Use files for large config values. Read them from disk instead of env vars.\n\n## 10. Env vars are inherited by child processes\n\n\n const child = spawn(\"node\", [\"worker.js\"])\n // child inherits ALL of parent's env vars\n\n\nThis means every child process gets your secrets, your database credentials, your API keys — even if the child doesn't need them. If the child is a third-party CLI tool or something you don't fully control, those secrets are now in its memory space.\n\n**How to handle it** : Be explicit. Pass only what's needed:\n\n\n\n spawn(\"node\", [\"worker.js\"], {\n env: { ONLY_WHAT_THEY_NEED: \"value\" }\n })\n\n\nOr use CtroEnv's secret masking to at least prevent accidental logging of sensitive values.\n\n_Links: GitHub | Docs_",
"title": "10 Things Nobody Tells You About process.env"
}