DEV Community

Cover image for The dotenv gotcha that will eat an afternoon of your life.
Daniel Akudbilla
Daniel Akudbilla

Posted on

The dotenv gotcha that will eat an afternoon of your life.

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 raw curl with it returned 200.
  • 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.
Enter fullscreen mode Exit fullscreen mode

๐Ÿงช 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));
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then track down where it's exported and remove the line:

grep -rn OPENAI_API_KEY ~/.z* ~/.bash* 2>/dev/null
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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)