If you've changed a value in .env and your app keeps using the old one, this is your bug.
TL;DR: dotenv will not overwrite an environment variable that's already set in your shell. If your .env edits aren't taking effect, something else already claimed that variable first. Fix: dotenv.config({ override: true }).
Here's a bug that looks impossible until you know the one rule behind it. dotenv will not overwrite an environment variable that's already set.
That's really the whole article. But the way this bites people is worth walking through, because the symptoms point everywhere except the actual cause.
๐ The symptom
I had an app reading its OpenAI key from a .env file. Every API call came back 429 insufficient_quota, the classic "you're out of credits" error.
So I checked the obvious things:
- The key in
.env? Valid. A rawcurlwith it returned200. - Billing on the account? Fine.
- The right model? Yep.
The same key worked from curl and failed from the app. Same machine, same file, same key. That combination shouldn't be possible, until you realize the app and the curl call weren't actually using the same key at all.
๐ต๏ธ The cause
My shell had an old OPENAI_API_KEY exported from a previous experiment, a dead key with no quota left. My curl test used the key directly, so it used the good one. But when the app loaded .env with dotenv, dotenv saw that process.env.OPENAI_API_KEY was already set by the shell, and left the dead key in place.
The .env file was correct the whole time. It was just being ignored.
// This is what everyone writes:
import 'dotenv/config';
// And this is dotenv's rule:
// if process.env.FOO already exists, the .env value is DISCARDED.
๐งช Prove it
Proving it takes about ten seconds. Print the key your code actually sees versus the one in the file:
import 'dotenv/config';
console.log('using key ending in:', process.env.OPENAI_API_KEY?.slice(-6));
If that tail doesn't match your .env, you've found it โ
. Something in your shell (~/.zshrc, ~/.zshenv, a docker run -e, a CI secret, a launchctl setenv) is shadowing the file.
๐ก Why dotenv does this on purpose
This isn't a bug. It's standard twelve-factor app behavior, and it's the right default. Real environment variables should outrank a local file. In production, you want the value your platform injected (from Kubernetes, ECS, Heroku, your secrets manager) to win over a stray .env that got left in the image by accident.
The trap only shows up in development, where a leftover export in your dotfiles quietly outranks the file you're actively editing. You change .env, nothing happens, and you start suspecting the network, the API, or the phase of the moon.
๐ ๏ธ The fixes
Fix 1: override: true
|
Fix 2: clean your shell | |
|---|---|---|
| Best for | Local dev, small/medium apps | Matching prod behavior exactly |
| Effort | One line, once | Manual cleanup, ongoing discipline |
| Risk | None in prod (no .env there to override) |
Easy to forget and re-export later |
Fix 1: make your file win (dev-friendly)
import dotenv from 'dotenv';
dotenv.config({ override: true });
Now .env beats the shell during local development. It's still safe in production. If there's no .env file on the server, there's nothing to override, so your platform's real environment variables win by default.
Fix 2: keep dotenv's default behavior, clean your shell instead
echo "$OPENAI_API_KEY" # find the stale value
unset OPENAI_API_KEY # kill it for this session
Then track down where it's exported and remove the line:
grep -rn OPENAI_API_KEY ~/.z* ~/.bash* 2>/dev/null
Reach for this option if you specifically want real environment variables to keep top priority, matching production behavior exactly.
๐ฏ The takeaway
When a config value refuses to change no matter what you put in .env, don't debug the value. Debug which value your process is actually reading:
console.log(process.env.THE_VAR?.slice(-6));
One line, and it turns an afternoon of staring at billing dashboards into a ten-second "oh" ๐
. Environment variables set outside your app always outrank your .env, unless you explicitly say otherwise. Now you won't forget it.
If this saved you an afternoon, you probably know someone else who's about to lose one too. Share the word
Happy Coding!
Top comments (0)