Add a Feature Flag to Control Releases

Add a feature flag to control releases in your CI/CD pipeline. Learn how feature flags decouple deployment from release, enabling safer rollouts and instant rollback.

Focus: add a feature flag to control releases

Sponsored

You've CI/CD'd your way through pipeline anatomy, artifacts, and approvals — but when the deploy is live, you still can't breathe easy. The staging rollout worked, the smoke tests passed, yet the moment you flip that production switch, you realize the new checkout flow is a disaster. Your only option is a full redeploy of the old version, and that's slow and risky. What if, instead, you could hide that feature behind a digital curtain — release the code to production safely, then turn it on only when you're ready, and flip it off instantly if something goes wrong? That's exactly what a feature flag does, and in this lesson, you'll learn to add one to control your releases, making your CI/CD pipeline not just a delivery engine, but a control center.

The problem this lesson solves

You've got a shiny new feature, you've covered it with tests, your pipeline is green, and you're proud. You merge to main, the pipeline runs, and the artifact is promoted to production. The deployment succeeds — meaning your new code is now running in front of every user. But you've just handed control of your users' experience to a binary event: deploy = release. This is the fundamental problem this lesson addresses. When deploy and release are the same thing, you lose flexibility. A simple typo in a rare code path becomes a full outage. A slow database query under load becomes an emergency rollback. Plus, rolling back a deploy often means reverting code changes, rebuilding artifacts, and waiting for the pipeline to cycle again — minutes or even hours of downtime that your users feel.

The pain is real: you can't test a feature with a small group of users, you can't A/B test easily, and you can't stage a complex migration. You're forced to commit to a feature's readiness at the exact moment you commit to deployment. In a world where delivery speed matters, this coupling is a bottleneck. Feature flags decouple the two, giving you a kill switch and a gradual rollout mechanism that doesn't require a single line of code to change after the deploy. As a CI/CD practitioner, you should see them not as a nice-to-have but as a core practice for high-velocity teams.

Why now? You've learned to build and promote artifacts; feature flags are the natural next step to manage the risk of those promotions. They're the difference between a turboprop (slow but rugged) and a jet (fast and agile) when it comes to release strategy.

Core concept / mental model

Think of your feature code as a light bulb. The code is the bulb's wiring and filament — it exists in the socket. The feature flag is the switch. Deploying the code is turning on the power to the socket; the bulb can still be off if the switch is in the "off" position. Similarly, a feature flag is a conditional that checks a configuration value (or remote service) at runtime. If the flag is on, the new code path executes; if off, the old code path runs.

This is a simple yet powerful mental model: deploy the code, release the feature are separate verbs. Your pipeline's job is to deploy — to make the code available on the servers. The feature flag's job is to release — to make the feature visible to users. By separating these, you gain:

  • Instant rollback: Flip the flag off instead of redeploying the old version.
  • Progressive delivery: Turn the flag on for 10% of users, then 50%, then 100%.
  • Testing in production: Start with internal users or a beta group.
  • Deployment risk reduction: If the feature has a bug, you can isolate it without delaying other releases.

Two types of flags exist: static flags (a simple hardcoded boolean in your codebase) and dynamic flags (external configuration via env vars, files, or a feature flag service like LaunchDarkly, Flipt, or Unleash). For CI/CD, dynamic flags are more useful because you can change them without redeploying or rebuilding — often via an API or a UI.

The mental model extends to your pipeline. In CI you can run tests for both flag states (on/off). In CD you can deploy with the flag off, then turn it on after you've verified health endpoints or log dashboards. This turns your release process from a risky jump into a series of careful, observable steps.

How it works step by step

Imagine you're adding a new, experimental search algorithm to your Python web service. Here's how you'd use a feature flag to control its release, step by step:

  1. Define the flag: Create a configuration placeholder for a flag, e.g., NEW_SEARCH_ALGORITHM_ENABLED, initially set to false. This can be an environment variable, a JSON file, or a record in a feature flag service.
  2. Implement the conditional: In your code, wrap the new feature behind a check for this flag. If the flag is on, use the new algorithm; otherwise, use the existing one.
  3. Deploy with flag off: Run your CI/CD pipeline, deploy the artifact to production, and leave the flag off. Users see the old behavior, but the new code is live and ready.
  4. Monitor the deploy: Check logs, metrics, and health endpoints to ensure the new code is stable — even though the feature isn't active.
  5. Turn the flag on: When you're confident, flip the flag to true (via an environment variable change, a config file update, or a service UI). The feature activates instantly without a new deploy.
  6. Monitor and ramp: Watch error rates and performance. If problems arise, flip the flag back to false — instant rollback.
  7. Clean up: Once fully rolled out, remove the old code path and the flag to keep your codebase clean.

Each step ties back to your CI/CD pipeline. For example, you can add a pipeline stage that runs tests with the flag both on and off to ensure compatibility. You can also make your deployment script update the flag automatically based on its value in an environment-specific config.

Hands-on walkthrough

Let's practice with a minimal Python Flask app. We'll create a feature that returns a new greeting message, controlled by an environment variable NEW_GREETING_ENABLED. We'll then simulate deploying with the flag off and turning it on without redeploying.

First, set up the app:

# app.py
import os
from flask import Flask, jsonify

app = Flask(__name__)

# Feature flag: disabled by default
NEW_GREETING_ENABLED = os.environ.get("NEW_GREETING_ENABLED", "false").lower() == "true"

def get_greeting():
    if NEW_GREETING_ENABLED:
        return "Hello from the new and improved endpoint!"
    return "Hello, world!"

@app.route("/")
def index():
    return jsonify({"message": get_greeting()})

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

Now, run the app with the flag off:

$ NEW_GREETING_ENABLED=false python app.py
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:5000

In another terminal, hit the endpoint (or open a browser):

$ curl http://localhost:5000
{"message":"Hello, world!"}

Now, without changing code, restart the app with the flag on (in a real scenario you'd change the environment variable in your deployment config or use a dynamic service):

$ NEW_GREETING_ENABLED=true python app.py

And verify:

$ curl http://localhost:5000
{"message":"Hello from the new and improved endpoint!"}

You just release-controlled a feature! The same artifact (your Python code) runs both old and new behaviors based on a runtime variable.

For a more robust approach, you can use a config file that your deployment process updates. Here's an example using a JSON config:

# config_loader.py
import json
import os

def load_flag(flag_name, default=False):
    config_path = os.environ.get("FLAG_CONFIG", "config.json")
    try:
        with open(config_path) as f:
            config = json.load(f)
        return config.get(flag_name, default)
    except (FileNotFoundError, json.JSONDecodeError):
        return default

This lets you deploy a new config.json (maybe via a separate pipeline step) to change flags without restarting the app — though a restart or hot-reload is still needed for Python unless you use a service.

Compare options / when to choose what

Approach Implementation Effort Dynamic Control Best For Considerations
Hardcoded bool Low None Quick experiments, local dev Requires code change + redeploy to flip
Environment variable Low Medium (restart needed) Simple services, containerized apps (Docker) Can't change without redeploy or restart
Config file (local) Low-Medium Medium (may need restart/watch) Small teams, simple flag sets Versionable, but not centralised
Feature flag service Medium-High High (instant via API/UI) Large teams, progressive rollouts, A/B tests External dependency, cost, complexity

When to choose what? If you're just starting, environment variables are a great entry point because they're easy, and many platforms (like Heroku, Docker, AWS) support them natively. Once you need to ramp users incrementally or want to avoid restarts, a feature flag service like LaunchDarkly or an open-source alternative like Flipt or Unleash shines. For compliance or air-gapped environments, self-hosted options matter.

Pro tip: Even with a feature flag service, keep a fallback to environment variables so your app doesn't crash if the service is unreachable.

Troubleshooting & edge cases

Flag always on/off: Check the environment variable type — a string "false" is truthy in Python! Always compare to "true" as we did. Otherwise NEW_GREETING_ENABLED="false" would still enable the feature.

Race conditions in multi-threaded apps: If you read the flag once at startup and then change it dynamically, the app won't pick up the change until restart. For dynamic flags, you'd use a service that checks on each request (with caching to avoid latency).

Flag leakage: If you log the flag's value, be careful not to expose sensitive config. Use timestamps and user IDs instead.

Dead code accumulation: If you never remove old flags, your codebase becomes cluttered. Schedule "flag cleanup" as part of your regular refactoring, and use tools like flag naming conventions to track.

Testing both states: A common mistake is only testing the flag-off path. Your CI pipeline should run tests twice — once with each flag state — to ensure both code paths are sound.

Feature flag service outage: If you rely on a remote service and it goes down, you need a fallback default (usually off for safety). Implement a local cache and sensible defaults.

What you learned & what's next

You've just unlocked a powerful release technique: adding a feature flag to control releases. You now understand that deployment and release are separate steps, and you've seen how to use environment variables or config files to toggle features at runtime. You can practice with your own apps, and you know the trade-offs between simple flags and full service solutions. You've also picked up crucial troubleshooting tips — like avoiding the classic "false" is truthy bug — and you've seen why testing both flag states matters.

This is the 26th step in your CI/CD foundations path. You've mastered pipeline anatomy, artifacts, approvals, and now feature flags. What's next? The natural progression is progressive delivery — taking feature flags to the next level with canary releases and A/B testing, where you automatically ramp traffic based on metrics. You'll learn how to combine feature flags with metrics and monitoring to make data-driven rollout decisions. That's the true power of CI/CD: not just fast, but safe and smart.

Practice recap

Take a simple Python function and wrap it behind an environment-variable-based flag. Deploy it with the flag off, then flip it on and observe the behavior change. Bonus: write a CI script that runs your tests twice, once with the flag on and once off.

Common mistakes

  • Treating NEW_FLAG=false as false — in Python, any non-empty string is truthy, so always check == "true".
  • Only testing the flag-off state in CI; you need to run tests for both flag conditions to catch regressions.
  • Forgetting to add a fallback default (like "false") so the app doesn't break if the flag variable is unset.
  • Relying on a dynamic flag service without a local cache or default, which causes outages when the service is unreachable.
  • Never cleaning up old flags, leading to dead code and confusion as flags accumulate.

Variations

  1. Use a config file (JSON or YAML) that your deployment process updates, instead of environment variables.
  2. Adopt a hosted feature flag service like LaunchDarkly or open-source alternatives (Flipt, Unleash) for dynamic runtime control without restarts.
  3. Implement a simple in-memory flag cache that refreshes from a config file at regular intervals, allowing hot toggling.

Real-world use cases

  • A social media app rolls out a new timeline algorithm to 10% of users using a feature flag, then ramps to 100% after monitoring click-through rates.
  • An e-commerce platform hides a new payment method behind a flag during deployment, enabling instant rollback if fraud rates spike.
  • A SaaS startup uses a feature flag to enable a redesigned onboarding flow only for new paid users, while keeping the old flow for existing users.

Key takeaways

  • Feature flags decouple deploy from release, giving you instant rollback and progressive rollout.
  • Start with environment variables for simplicity, then move to a feature flag service for dynamic control.
  • Always treat flag values as strings; compare to true rather than relying on truthiness.
  • Test your code with both flag states in CI to ensure no regressions in either path.
  • Clean up old flags after full rollout to avoid code clutter and maintenance overhead.
  • Feature flags are the foundation for advanced release strategies like canaries and A/B tests.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.