Feature Flags vs Environment Variables: When to Use Each
Environment variables and feature flags both put a condition in your code that changes behaviour without a code edit. That surface-level similarity causes a lot of teams to treat them as interchangeable, and they run into problems that are entirely avoidable.
They're not the same tool. They solve different problems, at different points in the deployment lifecycle, with different tradeoffs. Using the wrong one creates friction that compounds over time.
This guide draws a clear line between them.
What environment variables actually are
An environment variable is a value injected into your process at startup time by the host environment: your OS, your CI runner, your container orchestrator, or your deployment platform.
# .env.production
DATABASE_URL=postgres://prod-db:5432/myapp
STRIPE_SECRET_KEY=sk_live_...
NEXT_PUBLIC_API_BASE_URL=https://api.myapp.com
SENTRY_DSN=https://abc123@sentry.io/456
The critical word is startup. Your app reads these values once when the process boots. Changing them requires restarting the process, which in most deployment pipelines means a new deployment.
Environment variables are for configuration that:
- Is stable for the lifetime of a running process
- Varies between environments (development, staging, production), not between users
- Contains secrets or infrastructure references (API keys, database URLs, service endpoints)
- Is known at deploy time
What feature flags actually are
A feature flag is a condition evaluated at request time against values stored in an external system. The decision can depend on who is asking, when they're asking, and what percentage of traffic should see each variant.
// Evaluated fresh on every request (no restart needed)
const showNewCheckout = await client.getFeature('checkout.redesign', {
userId: user.id,
plan: user.plan,
email: user.email,
})
The flag value comes from a service (like Supaship) that you can update from a dashboard. The change takes effect for the next request with no deployment, no restart, and no code change.
Feature flags are for decisions that:
- Need to change without a deployment
- Vary per user, segment, or percentage of traffic
- Are temporary (rollouts, experiments, kill switches)
- Need to be reversed instantly if something goes wrong
The key differences
| Dimension | Environment variable | Feature flag |
|---|---|---|
| Evaluated at | Process startup | Request time |
| Changes take effect | After redeployment | Instantly, no restart |
| Varies by | Environment (dev/staging/prod) | User, segment, percentage, plan |
| Best for | Secrets, infra config, API keys | Rollouts, experiments, kill switches |
| Lifespan | Permanent (for that environment) | Temporary; cleaned up after rollout |
| Visibility | Baked into the process | Dashboard, audit log, change history |
| Rollback speed | Minutes (new deployment) | Seconds (dashboard toggle) |
| User targeting | No | Yes |
Where teams go wrong
Using env vars as feature flags
This is the most common mistake. It looks like this:
# .env.production
ENABLE_NEW_DASHBOARD=true
ENABLE_BETA_SEARCH=false
ENABLE_AI_ASSISTANT=true
if (process.env.ENABLE_NEW_DASHBOARD === 'true') {
return <NewDashboard />
}
It works, until it doesn't. Problems that accumulate:
Changing a value requires a deployment. If the new dashboard has a bug at 11pm, disabling it means writing a PR, merging, waiting for CI, and deploying. That's a 15–30 minute incident extension at minimum. A feature flag is a dashboard toggle you can hit in 10 seconds.
There's no per-user control. You can't roll out to 5% of users. You can't enable it for your beta testers while keeping it off for everyone else. You can't target users by plan or segment. The flag is either on for all prod traffic or off for all prod traffic, with no middle ground.
There's no audit trail. Who set ENABLE_NEW_DASHBOARD=true? When? Why was it changed? Environment variable changes disappear into deployment history. A feature flag platform records every change with a timestamp and the actor who made it.
Env vars accumulate and rot. Feature rollout env vars never get cleaned up. Three years later the codebase has ENABLE_LEGACY_FLOW_V2=false and no one knows what it does or whether it's safe to remove.
Using feature flags as environment config
The opposite mistake is less common but worth naming. Storing secrets, infrastructure URLs, or values that are stable per environment in a feature flag platform is the wrong tool:
// Don't do this
const dbUrl = await client.getFeature('database-url')
const stripeKey = await client.getFeature('stripe-secret-key')
Feature flags are evaluated per request, so adding a network call to fetch your database URL on every request introduces unnecessary latency and a new failure mode. Secrets don't belong in a flag platform's storage model. And these values don't need per-user targeting or instant rollback. They just need to exist per environment.
The right tool for common scenarios
"I need a different API endpoint in staging vs production"
→ Environment variable. This is exactly what env vars are for: stable, environment-specific config.
# .env.staging
NEXT_PUBLIC_API_URL=https://api-staging.myapp.com
# .env.production
NEXT_PUBLIC_API_URL=https://api.myapp.com
"I'm launching a redesigned onboarding flow and want to roll it out to 10% of new users"
→ Feature flag. You need per-user control, a gradual rollout, and the ability to kill it instantly if something's wrong.
const newOnboarding = await client.getFeature('onboarding.redesign-v3', {
userId: user.id,
createdAt: user.createdAt,
})
"I want to enable a feature only for paying customers on the Pro plan"
→ Feature flag. This is attribute-based targeting, a core feature flag use case that env vars cannot do.
const aiFeatures = await client.getFeature('features.ai-assistant', {
userId: user.id,
plan: user.plan, // targeting rule: plan === 'pro' || plan === 'enterprise'
})
"I have a Stripe webhook secret that needs to be in production"
→ Environment variable. Secrets are env vars. Full stop.
STRIPE_WEBHOOK_SECRET=whsec_...
"The new payment provider integration is unstable and I want a kill switch"
→ Feature flag. A kill switch needs to be actionable in seconds during an incident, not minutes after a deployment.
const useNewPaymentProvider = await client.getFeature(
'payments.provider-v2.enabled',
)
const provider = useNewPaymentProvider ? newProvider : legacyProvider
"I'm building a dark mode toggle that users control themselves"
→ Neither. That's a user preference stored in your database or localStorage. Feature flags control what the app makes available, not what individual users choose within it.
Using both together: the common pattern
In practice, the two tools complement each other. Environment variables handle the stable, deployment-time configuration. Feature flags handle the runtime, per-user decisions.
// lib/features.ts
import { SupaClient } from '@supashiphq/javascript-sdk'
// Environment variable: stable API key, set at deploy time
export const featureClient = new SupaClient({
apiKey: process.env.SUPASHIP_API_KEY!, // ← env var: infrastructure config
environment: process.env.NODE_ENV!, // ← env var: environment name
features: FEATURE_FLAGS,
context: {},
})
// In a request handler
const features = await featureClient.getFeatures(
['new-checkout', 'ai-assistant', 'beta-search'],
{
userId: user.id, // ← feature flag: per-user evaluation
plan: user.plan,
},
)
The API key that connects to Supaship is an environment variable. What the SDK returns (which features this specific user sees) is determined by feature flags.
A simple decision rule
Before adding a configuration value, ask two questions:
-
Does it vary between users or traffic segments?
If yes → feature flag. -
Does it need to change without a deployment or restart?
If yes → feature flag.
If the answer to both is no (it's stable per environment and set at deploy time), use an environment variable.
Feature flags and environment variables are both levers, but they operate at different levels of the stack. Using each for what it's designed for keeps your codebase clean, your incidents manageable, and your team able to ship with confidence.
Ready to replace your feature-flag env vars with a proper platform? Sign up for Supaship free · 1M events/month, per-user targeting, instant rollbacks, and an audit log. Pro plan is $30/month for your entire workspace.
Related reading:
- What are feature flags?: fundamentals and core concepts
- Complete Guide to Feature Flag Patterns: gradual rollouts, kill switches, beta programs, and more
- Feature Flags in Production: monitoring, lifecycle management, and pitfalls
Feedback
Got thoughts on this?
We're constantly learning how developers actually use these tools. Ideas, use cases, integration requests — every bit of feedback makes the platform better for everyone.
Thanks for being part of the journey — Supaship