Using Feature Flags for Safer React Production Deployments
Feature flags transform your deployment pipeline: instead of deploying and praying, you deploy with flags OFF, then enable them for a small percentage of users and watch. This article covers integrating feature flags into your CI/CD pipeline, coordinating deployments with flag rollouts, and best practices for using flags as your primary safety net in production.
In 2022, my team deployed a broken bundle to 100% of users at 2 PM Friday. The build succeeded, tests passed, but a stale cache somewhere in our CDN caused a JavaScript error for 40% of users. Chaos. If we'd deployed with a default-off flag and rolled out gradually, we'd have caught the error 10 minutes in, on 1% of users, and rolled back in 30 seconds. Now, feature flags are mandatory for all deployments—we never ship code at 100% on first deploy.
Deployment Pipeline with Feature Flags
A safe deployment pipeline looks like this:
1. Engineer pushes code to GitHub
2. CI runs tests, lint, type checks
3. If tests pass, build and upload to CDN
4. Deploy to production (with feature flags OFF for all risky features)
5. Verify health checks pass
6. Gradually enable flags: 1% → 5% → 25% → 50% → 100%
7. Monitor at each stage
8. Keep flags as kill switches for 24 hours
9. Delete or archive flags after 1 week
This is deployment-driven development (not test-driven, but safety-driven): safety is a first-class concern at every stage.
Integrating Flags into Your CI/CD
Your CI/CD system (GitHub Actions, GitLab CI, CircleCI) should know about flags. When you merge a PR, the pipeline should:
- Deploy the code (with flags OFF).
- Run smoke tests against the new code (via feature flag override in tests).
- Alert the on-call team that a deployment is ready.
- Wait for manual approval to enable flags.
Example GitHub Actions workflow:
# .github/workflows/deploy.yml
name: Deploy React App
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build React app
run: npm run build
- name: Upload to CDN
run: |
aws s3 sync dist/ s3://my-app-cdn/
aws cloudfront create-invalidation --distribution-id ${{ secrets.CF_DIST_ID }} --paths "/*"
- name: Deploy with flags OFF
run: |
curl -X POST https://flags.example.com/api/flags/batch \
-H "Authorization: Bearer ${{ secrets.FLAG_SERVICE_TOKEN }}" \
-d @- <<EOF
{
"updates": [
{ "flagName": "release_checkout_v2", "percentage": 0 },
{ "flagName": "release_dashboard_redesign", "percentage": 0 }
]
}
EOF
- name: Run smoke tests
run: npm run test:smoke
env:
# Override flags in test environment to test new code
FEATURE_FLAGS: '{"release_checkout_v2": true, "release_dashboard_redesign": true}'
- name: Notify Slack (ready for rollout)
uses: slackapi/slack-github-action@v1
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK }}
payload: |
{
"text": "Deployment ready for rollout",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "New code deployed with all flags OFF. Ready for manual rollout approval."
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": { "type": "plain_text", "text": "Approve Rollout" },
"value": "approve_rollout",
"url": "https://flags.example.com/flags/start-rollout"
}
]
}
]
}
manual-approval:
needs: build-and-deploy
runs-on: ubuntu-latest
environment: production # Requires manual approval
steps:
- name: Start progressive rollout
run: |
./scripts/start-progressive-rollout.sh
- name: Notify team (rollout started)
uses: slackapi/slack-github-action@v1
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK }}
payload: |
{
"text": "Progressive rollout started (1% of users)"
}
Key points:
- Flags OFF on deploy: All risky features are disabled for all users initially.
- Smoke tests with flags ON: Tests verify the new code works if flags are enabled.
- Manual approval gate: Before rolling out to users, require a team member to approve.
- Slack notifications: Keep the team informed of deployment status.
- Automatic rollout script: Start the progressive rollout (1% → 5% → ...) after approval.
Coordinating Code Deployment and Flag Rollout
A common question: when do I deploy code vs. enable flags?
Timeline:
T+0: Code deployed (flags OFF). Smoke tests pass.
T+5: Slack notification sent. On-call team reviews logs.
T+10: On-call approves rollout.
T+15: Flag enabled for 1% of users. Monitoring starts.
T+45: Error rate looks good. Expand to 5%.
T+75: Good. Expand to 10%.
T+105: Good. Expand to 25%.
T+165: Good. Expand to 50%.
T+225: Good. Expand to 100%.
T+225+24hrs: Flag stable for 24 hours. Delete flag (or keep as permanent kill switch).
Key insight: Code is deployed once, but the feature is "released" gradually via flags. This is the decoupling of deployment and release.
Handling Multiple Deployments in Flight
What if you deploy again before the previous rollout completes? Example:
T+0: Deployment #1 (checkout_v2). Flag enabled for 10%.
T+30: Deployment #2 (dashboard_redesign). Flag enabled for 1%.
Both deployments are live simultaneously. Feature flags handle this:
- Each flag is independent.
release_checkout_v2at 10%,release_dashboard_redesignat 1%. - Users see the right variant based on their flag assignments.
- Monitoring tracks each flag separately.
If deployment #1 has an issue, disable it (release_checkout_v2 → 0%). Deployment #2 continues unaffected.
In your code:
export function Dashboard() {
const checkoutV2 = useFeatureFlag('release_checkout_v2', false);
const dashboardRedesign = useFeatureFlag('release_dashboard_redesign', false);
return (
<div>
<Header />
{checkoutV2 && <CheckoutV2 />}
{!checkoutV2 && <CheckoutLegacy />}
{dashboardRedesign && <DashboardRedesigned />}
{!dashboardRedesign && <DashboardLegacy />}
</div>
);
}
Flags are evaluated independently, so multiple concurrent deployments are safe.
Monitoring and Alerting in Production
Once a feature is rolled out to users, monitoring is critical. Set up alerts for:
- Error rate by feature flag: Alert if error rate for a variant increases.
- Latency by feature flag: Alert if latency for a variant increases.
- Business metrics by feature flag: Alert if conversion/revenue by variant regresses.
- Infrastructure metrics: Database load, API latency, cache hit rate.
Example Datadog alert:
# Alert if checkout_v2 error rate > 2x baseline
name: "Checkout V2: Error rate spike"
query: |
(avg:app.errors{flag:release_checkout_v2:variant} /
avg:app.errors{flag:release_checkout_v2:control}) > 2
threshold: 1
notify: "@pagerduty-oncall"
Dashboard for all active flags:
Dashboard: "Feature Flags: Production Health"
Rows:
- Error Rate (by flag)
- Latency p95 (by flag)
- Conversion Rate (by flag)
- Database Query Time
- API Success Rate
- Cache Hit Ratio
- Revenue (by flag)
Add this dashboard to your team's operations center so all deployments are visible at a glance.
Fallback and Recovery
If monitoring detects an issue:
- Immediate: Alert the on-call engineer (Slack, PagerDuty).
- Diagnosis (< 5 min): Check error logs. Is the error new (caused by the variant) or existing (unrelated)?
- Decision (< 5 min): If clearly caused by the variant, disable the flag (percentage → 0).
- Recovery (< 5 min): Users see legacy behavior. Incident resolved.
- Investigation (next day): Root-cause the issue. Fix the code. Redeploy with flags OFF. Re-run rollout from 1%.
Without feature flags, step 3 requires a revert commit, rebuild, and redeploy (~15 minutes). With flags, it's a single API call (~30 seconds).
Testing Deployment Safety
Quarterly, run a deployment safety drill:
- Deploy a "broken" feature (e.g., intentionally throw an error in a feature flag path).
- Enable the flag for 1% of users.
- Verify that monitoring detects the error.
- Disable the flag and verify the error goes away.
- Post-mortem: Did monitoring alert fast enough? Was rollback easy?
This ensures your deployment safety infrastructure is working as expected.
Key Takeaways
- Always deploy with risky features flagged OFF; enable via progressive rollout (1% → 5% → ... → 100%).
- Integrate flags into your CI/CD pipeline: deploy → smoke tests → flag rollout approval → progressive rollout.
- Monitor error rate, latency, and business metrics at each rollout stage.
- Disable features instantly if monitoring detects issues. No redeploy needed.
- Keep deployments and releases decoupled: deploy once, release gradually.
- Use flags as permanent kill switches (ops toggles) for high-risk features.
Frequently Asked Questions
How do I handle database schema changes with feature flags?
Deploy schema changes in a backward-compatible way before code changes. For example, add a new column in migration #1 (nullable), deploy code that reads/writes it, migrate data if needed. This way, old code and new code coexist during the rollout. After the feature is 100% rolled out and stable, clean up old columns.
What if a flag needs a rollout longer than 1 week?
That's fine. Keep the flag enabled at 100% for longer (e.g., 2 weeks) before deleting it. This is especially true for critical features (payment, auth). As a rule of thumb: the riskier the feature, the longer you keep it as a permanent ops toggle.
Can I A/B test while rolling out a feature?
Yes. Use two flags: one for the release (release_feature_x at 1% → 100%), and one for the experiment (exp_feature_x_variant_a_vs_b). Users in the experiment are a subset of those in the release. You measure the effect of the variant within the rollout population.
How do I document which flags are in production?
Maintain a flag manifest (CSV or JSON) that lists: flag name, type (release/ops/exp), purpose, creator, created date, expected deletion date, and status (active/archived). Update it every time a flag is created or deleted. Example:
flagName,type,purpose,creator,createdDate,expectedDeletionDate,status
release_checkout_v2,release,Checkout redesign,[email protected],2026-06-02,2026-06-09,active
ops_payment_provider_fallback,ops,Kill switch for payment,devops,2025-01-01,,active
exp_homepage_variant_a_b,exp,Test new homepage design,[email protected],2026-05-15,2026-05-29,archived
How do I ensure flag rollouts don't happen at bad times (e.g., during high traffic or night)?
Schedule rollouts during low-traffic windows (e.g., 2–4 AM UTC, or designated release windows like Tuesday 10 AM). Use a deployment calendar so teams coordinate. Automated rollout scripts can be scheduled via cron jobs or your CI/CD system's scheduler.