Back to Blog
·5 min read

Why I stopped using environment variables for feature flags

Environment variables seem like the obvious choice for feature flags, but they create deployment headaches and limit runtime flexibility.

AI Dev
feature-flags
deployment
configuration
devops

Why I stopped using environment variables for feature flags

I used environment variables for feature flags for years. It felt natural -- set FEATURE_NEW_DASHBOARD=true in production, false in staging, deploy once, and toggle features across environments. Simple, version-controlled, and no external dependencies. Then I shipped a critical bug fix that required rolling back a feature flag, and I realized I'd have to redeploy the entire application just to change a boolean value. What should have been a 30-second fix became a 15-minute deployment with rollback risk.

The breaking point came during a Black Friday incident. Our new checkout flow was causing payment failures, and I needed to disable it immediately. But the feature flag was baked into environment variables, which meant triggering a full deployment pipeline while customers were actively trying to purchase. I watched revenue drop for 12 minutes while the rollback deployed, knowing I could have fixed it instantly with a proper feature flag system.

That's when I learned that environment variables optimize for deployment simplicity, not operational flexibility. Feature flags need to change independently of code deployments, especially when things go wrong.

The Environment Variable Problems

Environment variables create several operational challenges that are not obvious until you're under pressure:

Deployment coupling: Every flag change requires a full deployment. You can not quickly disable a broken feature without risking the stability of unrelated code changes that happen to be in the same deploy.

No runtime updates: If you need to toggle a flag based on real-time conditions -- server load, error rates, user feedback -- environment variables force you to redeploy instead of reacting immediately.

Limited targeting: Environment variables are binary per environment. You can not easily enable a feature for 10% of users, specific user segments, or particular geographic regions without complex application logic.

Restart requirements: Changing environment variables typically requires application restarts, adding downtime risk to what should be instant configuration changes.

Here's the pattern I used to use:

// config/features.ts
export const features = {
  newDashboard: process.env.FEATURE_NEW_DASHBOARD === 'true',
  advancedAnalytics: process.env.FEATURE_ADVANCED_ANALYTICS === 'true',
  betaCheckout: process.env.FEATURE_BETA_CHECKOUT === 'true',
}
 
// components/Dashboard.tsx
import { features } from '../config/features'
 
export function Dashboard() {
  if (features.newDashboard) {
    return <NewDashboard />
  }
  return <LegacyDashboard />
}

This works fine until you need to change FEATURE_BETA_CHECKOUT from true to false while customers are actively using the system.

My Current Feature Flag Approach

I switched to a hybrid approach that separates system configuration from runtime feature flags. Environment variables still handle system-level settings like database URLs and API keys, but feature flags live in a more flexible system.

For simple projects, I use a JSON configuration file served by a lightweight endpoint:

// api/feature-flags.ts
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const flags = {
    newDashboard: true,
    advancedAnalytics: false,
    betaCheckout: process.env.NODE_ENV === 'development',
  }
  
  res.setHeader('Cache-Control', 'public, max-age=60')
  res.json(flags)
}
 
// hooks/useFeatureFlags.ts
export function useFeatureFlags() {
  const { data: flags = {}, mutate } = useSWR('/api/feature-flags')
  
  return {
    flags,
    refresh: () => mutate(),
  }
}

For production systems, I use dedicated feature flag services like LaunchDarkly or PostHog. The key is that flag changes do not require deployments:

// lib/featureFlags.ts
import { LaunchDarkly } from '@launchdarkly/node-server-sdk'
 
const client = LaunchDarkly.init(process.env.LAUNCHDARKLY_SDK_KEY!)
 
export async function getFeatureFlag(
  key: string, 
  user: { id: string; email: string },
  defaultValue = false
) {
  await client.waitForInitialization()
  return client.variation(key, user, defaultValue)
}
 
// components/Dashboard.tsx
export function Dashboard({ user }: { user: User }) {
  const [showNewDashboard, setShowNewDashboard] = useState(false)
  
  useEffect(() => {
    getFeatureFlag('new-dashboard', user).then(setShowNewDashboard)
  }, [user])
  
  if (showNewDashboard) {
    return <NewDashboard />
  }
  return <LegacyDashboard />
}

What I Use Environment Variables For Now

I still use environment variables, but only for system configuration that should not change at runtime:

  • Database connection strings: DATABASE_URL
  • API keys and secrets: STRIPE_SECRET_KEY, JWT_SECRET
  • Service endpoints: REDIS_URL, ELASTICSEARCH_URL
  • Build-time flags: NODE_ENV, LOG_LEVEL

These are fundamentally different from feature flags because they define how the system operates, not what features it exposes to users.

The Operational Benefits

Since switching away from environment variables for feature flags, I've gained several operational advantages:

  • Instant rollbacks: Disable broken features in seconds, not minutes
  • Gradual rollouts: Enable features for small user percentages and monitor metrics
  • A/B testing: Run experiments without code changes or deployments
  • User targeting: Enable features for specific user segments, geographic regions, or subscription tiers
  • Real-time monitoring: Adjust feature availability based on system load or error rates

The most important benefit is confidence. I can ship features behind flags, enable them for internal testing, then gradually roll out to users while monitoring for issues. If something goes wrong, I can disable the feature instantly without touching the deployment pipeline.

Environment variables are still the right choice for system configuration, but feature flags deserve a more flexible infrastructure that matches their operational requirements.