Feature Flags

Learn how to use feature flags in GitLab for progressive delivery. From creating and toggling flags to canary deployments and Unleash integration, build release workflows that decouple deployment from release.

Feature Flags Progressive Delivery GitLab CI/CD
Why Feature Flags?

Traditionally, deploying code and releasing a feature are the same event: when the pipeline deploys, users see the new functionality. This coupling creates risk. A deploy that would otherwise be routine becomes risky because it might introduce a bug, and rolling back requires redeploying the previous version. Feature flags break this coupling: the code is deployed (dark launch), but the feature is disabled. When you're ready, you enable the flag, and users see the new functionality. If something goes wrong, you disable the flag — no redeploy needed .

This decoupling enables progressive delivery: releasing a feature to a small percentage of users first, monitoring for problems, and gradually increasing the percentage. It enables A/B testing, where different user segments see different versions. It enables canary deployments, where the new version is deployed to a subset of infrastructure and traffic is gradually shifted. And it enables dark launches, where the code is deployed and tested in production without users seeing it .

GitLab provides feature flags as part of its platform. You can create flags in the UI, toggle them on and off, associate them with environments, and control them via API from your application code. For more advanced use cases, GitLab integrates with Unleash, an open-source feature flag service that supports strategies, constraints, and gradual rollouts. The choice between GitLab's built-in flags and Unleash depends on your needs — built-in is simpler, Unleash is more powerful .

Core Principle: Deploy and release are separate. Deploy the code with the flag off; enable the flag when you're ready. Rollback by disabling the flag — no redeploy.

Feature Flag

A toggle that controls whether a feature is active. Stored in GitLab, evaluated by your application.

Progressive Delivery

Release gradually: 1% → 10% → 50% → 100%. Monitor at each step, roll back if needed.

Canary Deployment

Deploy the new version to a subset of infrastructure, shift traffic gradually. Feature flags control user exposure.

Unleash

Open-source feature flag service. Supports strategies, constraints, gradual rollouts. GitLab can proxy to Unleash.

Dark Launch

Deploy code with the flag off. Test in production without user exposure. Enable when ready.

SDK Integration

Application code evaluates flags via SDK (Unleash client, LaunchDarkly, etc.) or direct API call.
Creating and Managing Feature Flags

Feature flags are created in the GitLab UI: Deploy → Feature flags → New feature flag. Each flag has a name (used in code), a description, and a scope. The scope determines which environments the flag applies to — a flag scoped to production only affects production; a flag scoped to * applies everywhere. This allows you to test a flag in staging without affecting production, or to enable a flag in production without enabling it in all environments .

Flags can be toggled from the UI with a switch, or via API. The API is useful for automating the toggle — for example, a pipeline job can enable a flag after a successful deployment. The API also enables integration with other tools: you can toggle flags from a chat bot, from a monitoring system, or from a custom dashboard. The flag state is stored in GitLab and is the source of truth for the application .

Each flag has a version number, which is used for compatibility. When you toggle a flag, GitLab increments the version. The application should include the version in its API request and cache the response — if the version matches, the cached value is used; if not, the application fetches the new value. This minimizes API calls and ensures the application quickly picks up changes. The version also enables optimistic locking: if two clients try to update a flag simultaneously, the one with the older version is rejected .

# Creating a feature flag via UI # Deploy > Feature flags > New feature flag # - Name: new_checkout # - Description: Enable new checkout flow # - Environment scope: production # Feature flags API # List flags curl --header "PRIVATE-TOKEN: " \ "https://gitlab.example.com/api/v4/projects/:id/feature_flags" # Create flag curl --request POST \ --header "PRIVATE-TOKEN: " \ --url "https://gitlab.example.com/api/v4/projects/:id/feature_flags" \ --data "name=new_checkout" \ --data "description=Enable new checkout flow" \ --data "version=1" # Update flag (toggle) curl --request PUT \ --header "PRIVATE-TOKEN: " \ --url "https://gitlab.example.com/api/v4/projects/:id/feature_flags/new_checkout" \ --data "active=true" \ --data "version=2" # Delete flag curl --request DELETE \ --header "PRIVATE-TOKEN: " \ --url "https://gitlab.example.com/api/v4/projects/:id/feature_flags/new_checkout" # Get flag state via API (for application) # Note: This endpoint does not require authentication # if the flag is not scoped to a protected environment curl "https://gitlab.example.com/api/v4/features/new_checkout?scope=production" # Response: # { # "name": "new_checkout", # "state": "on", # "gates": [...] # } # Flag state in code: # - If state == "on": feature enabled # - If state == "off": feature disabled # - If flag not found: feature disabled (safe default) # Versioning: # - Increment version on every update # - Use version for caching and optimistic locking # - If version mismatches, refetch
Feature Flag Management Best Practices:
  • Use descriptive names (new_checkout, not flag1)
  • Scope flags to environments (staging, production) to avoid unintended exposure
  • Set a safe default (off) so unknown flags don't enable features
  • Clean up old flags — stale flags accumulate and cause confusion
  • Track flag usage in code — remove flags when they're no longer needed
Using Feature Flags in Application Code

Feature flags are useless without application code that reads them. The code must fetch the flag state from GitLab (or Unleash) and branch on it. There are two approaches: fetch the flag state on every request (slow, but always current) or cache it with a short TTL and the version number (fast, and still fresh). The caching approach is the standard: fetch the flag once, cache it for a minute, and refresh when the version changes .

The flag check should be fast and non-blocking. If the flag service is unavailable, the application should fall back to a safe default — usually "off." A feature flag check that blocks a request or throws an exception is worse than the feature itself. Use the SDK's built-in caching and circuit breaker, or implement a simple in-memory cache with a timeout, so the flag check never becomes the bottleneck.

Flag checks should be placed at the decision points: the controller action that renders the checkout page, the service method that processes the payment, the template that renders the new UI. Don't scatter flag checks throughout the code — they become hard to track and remove. Each flag should be checked in one (or a few) well-defined places, so when it's time to remove the flag, the change is localized.

# Node.js: Using Unleash client const { initialize } = require('unleash-client'); const unleash = initialize({ url: 'https://gitlab.example.com/api/v4/feature_flags/unleash/:id', appName: 'my-app', instanceId: 'my-app-production', refreshInterval: 60000, // Refresh every 60s customHeaders: { Authorization: '' } }); app.get('/checkout', (req, res) => { const useNewCheckout = unleash.isEnabled('new_checkout'); if (useNewCheckout) { return res.render('checkout-new'); } return res.render('checkout-old'); }); # Python: Using Unleash client from UnleashClient import UnleashClient client = UnleashClient( url="https://gitlab.example.com/api/v4/feature_flags/unleash/:id", app_name="my-app", instance_id="my-app-production", refresh_interval=60, custom_headers={"Authorization": ""} ) client.initialize_client() def checkout(): if client.is_enabled("new_checkout"): return render_template("checkout-new.html") return render_template("checkout-old.html") # Ruby: Using Unleash client require 'unleash' Unleash.configure do |config| config.url = 'https://gitlab.example.com/api/v4/feature_flags/unleash/:id' config.app_name = 'my-app' config.instance_id = 'my-app-production' config.custom_http_headers = { 'Authorization' => '' } end UNLEASH = Unleash::Client.new get '/checkout' do if UNLEASH.is_enabled?('new_checkout') erb :checkout_new else erb :checkout_old end end # Direct API call (no SDK) async function isEnabled(flagName) { const response = await fetch( `https://gitlab.example.com/api/v4/features/${flagName}?scope=production` ); const data = await response.json(); return data.state === 'on'; }
Flag Check Caveats: