Feature Flags
Progressive Delivery
Canary Deployments
Unleash Integration
Safe Releases
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:
- Never block on flag check: Use a timeout (e.g., 1s) and fall back to safe default
- Cache the flag state: Fetch once per minute, not once per request
- Centralize flag checks: One place per flag makes removal easy
- Use safe defaults: If the flag is unknown or the service is down, default to "off"
- Log flag changes: When the flag flips, log it — useful for debugging
Progressive Delivery with Feature Flags
Progressive delivery is the practice of releasing features gradually: to a small percentage of users first, then increasing. The flag controls the percentage — 1%, 10%, 50%, 100%. Each step is a separate toggle, giving you time to monitor for errors, performance issues, or user feedback. If something goes wrong, you disable the flag, and users instantly see the old behavior — no redeploy, no rollback of infrastructure .
The strategy is to start small and increase slowly. A 1% rollout might run for an hour; if no issues, 10% for a day; if no issues, 50% for a week; then 100%. The exact timing depends on your traffic volume and risk tolerance. The point is that you have control over the pace, and you can stop at any step. This is much safer than a 100% deploy that you have to roll back if something goes wrong.
Progressive delivery works best with monitoring. At each stage, you look at error rates, latency, conversion rates, and any other metrics that matter. If the metrics look normal, you proceed. If they degrade, you roll back. This requires that your metrics are segmented by flag state — you need to know whether users in the 1% bucket are experiencing issues, not just whether the overall system is healthy. Feature flag platforms like Unleash support this by exposing the flag state in metrics and traces .
# Progressive delivery rollout
# Step 1: Deploy with flag off
deploy:
script:
- kubectl set image deployment/my-app my-app=$IMAGE
- kubectl rollout status deployment/my-app
environment:
name: production
# Feature flag: new_checkout = off
# Users see old behavior
# Step 2: Enable flag for 1% of users
# (In UI or via API)
# new_checkout = on, rollout 1%
# Step 3: Monitor for 1 hour
# - Check error rate in monitoring
# - Check latency
# - Check user feedback
# Step 4: Increase to 10%
# Step 5: Monitor for 1 day
# Step 6: Increase to 50%
# Step 7: Monitor for 1 week
# Step 8: Increase to 100%
# Step 9: Remove the flag from code
# (After confirming new behavior is stable)
# - Delete flag in GitLab
# - Remove flag check in code
# - Deploy
# Canary deployment with feature flags
canary:
stage: deploy
script:
# Deploy canary version (new image) to 10% of pods
- kubectl set image deployment/my-app-canary my-app=$IMAGE
# Feature flag controls user exposure
environment:
name: production/canary
# Feature flag strategies (Unleash)
# - Standard: on/off globally
# - Gradual rollout: % of users
# - User IDs: specific users
# - IPs: specific IPs
# - Hosts: specific hosts
# - Custom strategies
# Gradual rollout (Unleash):
# - 1% of users see new feature
# - Increase over time
# - Roll back by setting to 0%
# A/B testing:
# - 50% see version A
# - 50% see version B
# - Measure conversion, engagement
Progressive Delivery Best Practices:
- Start at 1% or less; increase slowly
- Monitor error rate, latency, and business metrics at each step
- Have a rollback plan (disable the flag)
- Use consistent user targeting (same user always sees same version)
- Remove flags after full rollout (don't leave dead code)
Unleash Integration
GitLab's built-in feature flags support basic on/off toggling with environment scoping. For more advanced use cases — gradual rollouts by percentage, targeting specific users, custom strategies, A/B testing — GitLab integrates with Unleash. Unleash is an open-source feature flag service with a rich strategy system: you can define strategies like "gradual rollout by 10%," "target users with specific IDs," "target IPs in a range," or custom strategies that you implement .
The integration is straightforward: GitLab acts as a proxy to Unleash. You configure Unleash in GitLab settings (URL, API token), and then flags created in GitLab are stored in Unleash. Your application uses the Unleash SDK (not GitLab's built-in API) to evaluate flags, so it gets the full strategy engine. GitLab remains the UI for managing flags, and the deployment integration works the same way — flags are still associated with environments and can be toggled from GitLab .
The benefit of Unleash is power and flexibility. A gradual rollout strategy can be configured in seconds, and the Unleash SDK handles the user bucketing automatically. Custom strategies can be written in your language of choice and deployed as Unleash strategy plugins. For teams doing serious progressive delivery, Unleash is the right choice. For simpler needs, GitLab's built-in flags are sufficient.
# Configure Unleash in GitLab
# Settings > CI/CD > Feature flags > Unleash
# - Unleash URL: https://unleash.example.com/api/
# - Instance ID: my-app
# - Application name: my-app
# - Environment: production
# - API token:
# Install Unleash (self-hosted)
# docker run -d \
# -p 4242:4242 \
# -e DATABASE_URL=postgres://... \
# unleashorg/unleash-server
# Or use Unleash Cloud: https://www.getunleash.io/
# Unleash strategies (configured in Unleash UI)
# - default: on/off
# - userWithId: target specific users
# - gradualRolloutUserId: % of users
# - gradualRolloutRandom: % of requests
# - gradualRolloutSessionId: % of sessions
# - remoteAddress: target IPs
# - applicationHostname: target hosts
# - custom strategies
# Unleash client configuration (Node.js)
const { initialize } = require('unleash-client');
const unleash = initialize({
url: 'https://unleash.example.com/api/',
appName: 'my-app',
instanceId: 'my-app-production',
customHeaders: {
Authorization: ''
}
});
# Feature flag with gradual rollout (Unleash)
# In Unleash UI: new_checkout
# - Strategy: gradualRolloutUserId
# - Percentage: 10
# - GroupId: new_checkout
# This enables new_checkout for 10% of users
# A/B test (Unleash)
# In Unleash UI: checkout_variant
# - Strategy: gradualRolloutUserId
# - Percentage: 50
# - Variants:
# - control: 50% (old checkout)
# - treatment: 50% (new checkout)
# Custom strategy (Unleash)
# - Write a strategy in your language
# - Register with Unleash SDK
# - Configure in Unleash UI
# - Use in your flag checks
Unleash Integration Benefits:
- Gradual rollouts by percentage
- Targeting by user ID, IP, hostname
- A/B testing with variants
- Custom strategies
- GitLab remains the UI for flag management
Complete Progressive Delivery Pipeline
Here's a complete pipeline that deploys with a feature flag off, then progressively enables it.
# Complete progressive delivery pipeline
stages:
- build
- deploy
- release
variables:
FEATURE_FLAG_NAME: new_checkout
FEATURE_FLAG_SCOPE: production
# Build and push image
build:
stage: build
image: docker:24
services:
- docker:24-dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
# Deploy with flag off (dark launch)
deploy:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl set image deployment/my-app my-app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA -n production
- kubectl rollout status deployment/my-app -n production
environment:
name: production
# Feature flag is off; new code is deployed but inactive
# Enable flag for 1% of users
enable_1_percent:
stage: release
image: curlimages/curl:latest
script:
- |
curl --request PUT \
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
--url "https://gitlab.example.com/api/v4/projects/$CI_PROJECT_ID/feature_flags/$FEATURE_FLAG_NAME" \
--data "active=true" \
--data "strategies=[{\"name\":\"gradualRolloutUserId\",\"parameters\":{\"percentage\":\"1\",\"groupId\":\"new_checkout\"}}]"
environment:
name: production
when: manual
# 1% of users see new feature
# Enable flag for 10%
enable_10_percent:
stage: release
image: curlimages/curl:latest
script:
- |
curl --request PUT \
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
--url "https://gitlab.example.com/api/v4/projects/$CI_PROJECT_ID/feature_flags/$FEATURE_FLAG_NAME" \
--data "active=true" \
--data "strategies=[{\"name\":\"gradualRolloutUserId\",\"parameters\":{\"percentage\":\"10\",\"groupId\":\"new_checkout\"}}]"
when: manual
# Enable flag for 100%
enable_100_percent:
stage: release
image: curlimages/curl:latest
script:
- |
curl --request PUT \
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
--url "https://gitlab.example.com/api/v4/projects/$CI_PROJECT_ID/feature_flags/$FEATURE_FLAG_NAME" \
--data "active=true"
when: manual
# Disable flag (rollback)
disable_flag:
stage: release
image: curlimages/curl:latest
script:
- |
curl --request PUT \
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
--url "https://gitlab.example.com/api/v4/projects/$CI_PROJECT_ID/feature_flags/$FEATURE_FLAG_NAME" \
--data "active=false"
when: manual
# Users immediately see old behavior
Troubleshooting Feature Flags
# Flag not changing behavior
# - Check flag name matches exactly (case-sensitive)
# - Check flag scope matches environment
# - Check application is fetching the flag
# - Check caching TTL (may take up to TTL to update)
# Flag check fails (timeout, 5xx)
# - Check GitLab is reachable from the application
# - Check API token is valid
# - Check flag exists
# - Implement fallback to safe default (off)
# Flag state inconsistent across instances
# - Check all instances use the same flag name
# - Check all instances fetch from the same GitLab project
# - Check caching TTL is short (1-5 minutes)
# - Use version number to detect stale cache
# Unleash integration not working
# - Check Unleash URL is correct
# - Check API token is valid
# - Check the flag exists in Unleash
# - Check the SDK uses the correct URL (not GitLab's API)
# Gradual rollout not working
# - Check strategy is configured (gradualRolloutUserId)
# - Check percentage is set
# - Check groupId matches
# - Check user ID is stable (same user, same bucket)
# Flag removed from GitLab but code still checks it
# - Application should default to "off" if flag is missing
# - Remove the flag check from code
# - Deploy the updated code
# Debug: list flags via API
script:
- curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
"https://gitlab.example.com/api/v4/projects/$CI_PROJECT_ID/feature_flags"
# Debug: check flag state
script:
- curl "https://gitlab.example.com/api/v4/features/$FEATURE_FLAG_NAME?scope=production"
# Debug: check application flag client
script:
- echo "Flag state: $(curl -s https://gitlab.example.com/api/v4/features/$FEATURE_FLAG_NAME?scope=production)"
Frequently Asked Questions
What is the difference between deploying and releasing?
Deploying puts code on servers. Releasing makes the feature visible to users. Feature flags decouple the two: you deploy with the flag off, then release by enabling the flag.
What is progressive delivery?
Releasing a feature gradually: 1% → 10% → 50% → 100% of users. Each step is monitored; if issues arise, the flag is disabled and users see the old behavior. Much safer than a 100% deploy .
When should I use GitLab's built-in flags vs. Unleash?
Built-in flags are sufficient for simple on/off toggling with environment scoping. Use Unleash when you need gradual rollouts by percentage, user targeting, A/B testing, or custom strategies .
How do I roll back a feature?
Disable the flag. Users immediately see the old behavior. No redeploy, no rollback of infrastructure — just a toggle change .
How do I avoid flag tech debt?
Treat flags as temporary. After the feature is stable at 100%, remove the flag from the code and delete it in GitLab. Review flags quarterly and clean up stale ones.
Are feature flags secure?
The flag state is not sensitive, but the API token used to toggle flags is. Treat it as a secret. For sensitive features, use server-side flag checks, not client-side, so the code path is not exposed .
Can I use feature flags for infrastructure changes?
Feature flags are for application behavior. For infrastructure, use feature-flag-like mechanisms in your deployment tooling (e.g., canary deployments, traffic shifting) rather than application-level flags.
How do I test feature flags in CI/CD?
Use a test environment with flags enabled, or use the flag API to toggle flags before running tests. For unit tests, mock the flag client to return specific values.