Heroku environment variables are key-value pairs you set on an app with heroku config:set and read at runtime from the environment. That is the one-line answer. It is also the answer that hides the more interesting story: the simple way Heroku exposed app config in 2008 quietly became the de-facto mental model for environment variables on every PaaS that came after, including the ones that compete with Heroku directly. Most engineers under forty have never lived in a world where “config vars” was not a tab in a dashboard.
The reason it stuck is the same reason it still works: it is the closest thing to a perfect abstraction for application configuration. It hides secrets from source control, makes config changes a deploy-level event instead of a code change, and lets the same build artifact run in staging and production without rebuilds. The things that go wrong are almost always on the edges, not in the core idea.
The direct answer
To set Heroku environment variables, run heroku config:set KEY=value from the CLI, or open the Settings → Config Vars tab in the Heroku dashboard. To read them, either heroku config (CLI) or call os.environ.get("KEY") / process.env.KEY in your application. Config vars are injected as Unix-style environment variables into every dyno at boot, are available to all buildpacks, and survive across deploys and restarts.
For multi-value or multi-line values (private keys, JSON blobs, base64 strings), use the same commands and Heroku handles the wrapping. There is no size limit worth worrying about for app config, and no separate “production vs staging” config set — the variables live on the app, and the app is the environment.
Why this model won
Before Heroku, “where do I put my database password” was answered with one of three bad options. You committed it to source control and prayed. You put it in a config file and shipped the file with the deploy. You used a local_settings.py / config.local.json pattern that worked on your laptop and silently broke in production. Heroku’s config:set was the first mainstream answer that was both simple enough to use and safe enough to keep using.
The pattern it normalized is now standard:
- Config lives in the platform, not the repo.
- Config is set once per environment, not once per build.
- The same build artifact runs in every environment.
- Secrets never enter git history.
- Changing config does not require a code change.
That is, more or less, the third factor of the twelve-factor app manifesto, and Heroku is the reason most developers have heard of it. The fact that almost every modern PaaS — Render, Fly, Railway, RunxBuild, the big hyperscalers’ app services — has a “Config Vars” or “Environment” tab is the proof. The model is no longer Heroku’s. It is the industry’s.
The CLI basics worth keeping in muscle memory
# Set a single variable
heroku config:set DATABASE_URL=postgres://...
# Set several at once
heroku config:set NODE_ENV=production LOG_LEVEL=info
# Read all variables for the current app
heroku config
# Unset a variable
heroku config:unset DATABASE_URL
# Read a single value
heroku config:get DATABASE_URL
Two CLI behaviors that surprise people the first time:
- There is no staging-vs-production CLI flag.
heroku config:setalways targets the app you have linked withgit remote. To set a variable on a different app, you pass--app myapp-stagingor run the command from a directory that is linked to that app. The platform tracks which app you are talking to, not which environment. - Setting a variable triggers a restart. Heroku treats config changes the same as a code change for the purposes of dyno lifecycle. If your app is running and you
config:seta new env var, your dyno restarts within a few seconds. That is usually what you want, and it is occasionally not. The fix in the rare cases where it is not is to deploy the variable change with a code change and a flag-controlled rollout.
The five things that actually go wrong
Most Heroku environment variable tickets fall into one of these five buckets. Knowing the bucket is half the fix.
1. The variable was set, but the app was not restarted. Most language runtimes read the environment once at process start. If you config:set and your process is still serving the old value, a restart fixes it. The reliable way to confirm the value the dyno actually sees is heroku run env — that opens a one-off dyno and prints every variable, which is what the app would see at boot.
2. The variable was set in the wrong app. If you maintain a staging and production app, this is a one-letter typo away. The safety net is to put the app name in your shell prompt, or to wrap heroku config:set in a deploy script that asserts the app name before running the command.
3. The variable contains characters that the shell mangles. Single quotes, dollar signs, backticks, and newlines in a value can break a heroku config:set KEY=... call in subtle ways. The CLI is generally good at escaping, but the safest move for a private key or a large JSON blob is to pipe it through a file. Heroku also supports multi-line values directly:
heroku config:set PRIVATE_KEY="$(cat ./private.key)"
That double-quote with $(...) is the idiom. Plain cat without the quotes will mangle newlines.
4. The variable is read with the wrong name in the code. The most common cause of “I set the env var and the app is still using the default” is a typo, a case mismatch, or a leading underscore. Heroku itself is case-sensitive on variable names. DATABASE_URL and database_url are two different variables. Pick one case and enforce it in the codebase.
5. The variable is in the build but not the runtime. Buildpacks sometimes consume variables at build time (for example, a NPM_TOKEN used to install private packages) and do not pass them to the runtime. Heroku has a separate “Config Vars” and “Build Settings” concept, and a variable marked as build-only will not appear in the running app. Use heroku run env to confirm.
The DATABASE_URL pattern that quietly shaped everything
Heroku’s decision to expose a single DATABASE_URL string for database connections — and to standardize the format — was a quietly huge interoperability move. Before it, every language had its own config for database connections: DB_HOST, DB_PORT, DB_USER, DB_PASS, DB_NAME in Postgres; MONGO_URL in Mongo; a JDBC URL in Java. Heroku collapsed all of that to one variable, in one format, and every add-on in the marketplace had to honor it.
The same pattern shows up everywhere now:
- Redis add-ons inject
REDIS_URL. - Postgres add-ons inject
DATABASE_URL. - SMTP add-ons inject
SMTP_URL. - S3-compatible storage injects
S3_BUCKET,S3_KEY,S3_SECRET, and anS3_ENDPOINTfor non-AWS providers.
When you migrate an app off Heroku, the things that take the most time are almost never the framework code. They are the *_URL strings. The shortcut is to set the same env var names on the new platform that Heroku set, and the rest of the app does not need to know it moved.
What to keep when you leave Heroku
The interesting question for most teams in 2026 is not “how do I use Heroku env vars” but “what do I keep from the model when I build on something else.” A short list, in priority order:
- One variable, one source of truth. Resist the temptation to add
DB_HOSTandDB_PORTas a “backup” toDATABASE_URL. Pick the format you trust and stick to it. - Per-environment isolation. Staging and production must be separate. Not by namespace, not by prefix — separate. A typo in a variable name should never make a production app read a staging value.
- No env vars in code. Even as defaults.
os.environ.get("PORT", "8000")is a maintenance trap because it hides the fact that 8000 is a guess. A startup error on a missingPORTis the right behavior. - A
heroku run envequivalent. On any platform, the equivalent one-liner that prints the actual environment your app sees at boot is the single most useful debugging tool you have. Know the command on whatever you move to. - No secrets in build-time env vars that the runtime does not need. If the runtime does not need a private key, do not put it in the runtime env. The smaller the runtime surface, the smaller the blast radius.
The platforms that do this well, including RunxBuild, expose the same model: a single dashboard tab for environment variables, CLI commands that mirror the Heroku CLI shape, a one-off shell that prints the active environment, and per-environment isolation by app or by service. None of that is magical. It is all just the pattern Heroku popularized, kept intact.
If you are sizing a new project and want to know what each tier of a managed environment costs in real monthly numbers, the hosting calculator is a fast way to see the difference between always-on and spin-down pricing, and between managed and self-hosted database. For the broader operations picture — domains, logs, scaling, deploys, and how env vars fit into a service’s lifecycle — the services workflow documentation is the place to start.
FAQ
How do I set a Heroku environment variable without restarting the app?
You cannot. Heroku restarts dynos when config changes, on purpose. The closest workaround is to use a feature flag in your app and ship the flag change with the env var change in the same release, so the new value is read on the next deploy instead of an ad-hoc restart.
Can I have a .env file locally and config vars in production?
Yes, and that is the standard setup. Locally, a .env file (loaded by Foreman, dotenv, or your framework’s dev server) provides the same variables Heroku would inject in production. The trick is to keep .env in .gitignore and ship a .env.example with placeholder values so other developers know what to set. The production values never leave the platform.
Are Heroku config vars encrypted at rest?
Yes. Heroku stores config vars encrypted and only injects them into running dynos over their internal network. The values are not visible in the dashboard to other users on the account unless they have been granted access, and they are not in any build artifact or git history. The same model is used by most managed platforms.
What is the difference between heroku config:set and heroku labs:enable runtime-dyno-metadata?
config:set sets a variable on the app. runtime-dyno-metadata is a labs feature that injects a fixed set of variables (HEROKU_APP_ID, HEROKU_APP_NAME, HEROKU_DYNO_ID, HEROKU_RELEASE_VERSION, HEROKU_SLUG_COMMIT) automatically. It is useful for logs and for distinguishing dynos in a scaled-out app.
Can I use environment variables to do A/B testing or feature flags?
You can, and it is a common quick-and-dirty approach for simple toggles. For anything beyond a binary on/off, a real feature-flag service is worth the setup, because config-var-based flags require a config:set and a dyno restart to change, and that is the wrong speed for a flag you want to flip without a deploy.
Why is my Heroku env var showing in the dashboard but not in the app?
Three usual suspects. The app has not been restarted since you set it. The variable is set on a different app than the one you are running. Or the variable is set in build settings only, not in config vars. heroku run env from inside the app’s directory will print the full environment and tell you which one it is.
How long does it take for a config change to take effect?
Usually a few seconds. Heroku marks the release, restarts the dynos in a rolling fashion, and the new value is available to the next request. If you have a slow-startup app or a process that caches the env at boot, you may need a manual restart with heroku restart to be sure.
Closing thought
Heroku’s config vars are a small idea that got adopted at the right time and never got replaced. The platforms that competed with Heroku on price or on raw capability did not replace the model — they copied it, and the model improved. The thing worth carrying away from this whole history is not the CLI command, it is the discipline: config lives in the platform, secrets stay out of the repo, and the same build runs in every environment. The team that keeps that discipline on whatever platform they land on will have fewer incidents than the team that does not, regardless of which one is hosting the app.