DevOps Advanced

Feature Flags and Progressive Delivery in Django: Canary Releases, A/B Tests, and Kill Switches

Ship to production continuously without big-bang risk. Build a feature-flag layer in Django, roll features out to a percentage of users, run A/B experiments, and add instant kill switches for when something goes wrong.

DjangoZen Team Jun 06, 2026 3 min read 5 views

Deploying code and releasing a feature should be two separate events. Feature flags let you merge to main, deploy on Tuesday, and turn a feature on for 5% of users on Thursday — then roll it back in one click if metrics dip. This is progressive delivery, and it's how mature teams ship fast without breaking things.

Decouple deploy from release

Without flags, every deploy is a gamble — all code goes live to everyone at once. With flags, the deploy is boring (the new code is dormant) and the release is a controlled, reversible config change. That separation is the entire point.

A minimal flag layer

class FeatureFlag(models.Model):
    key = models.SlugField(unique=True)
    enabled = models.BooleanField(default=False)
    rollout_percent = models.PositiveSmallIntegerField(default=0)  # 0-100
    allowed_users = models.ManyToManyField(User, blank=True)

Resolve a flag deterministically — the same user always gets the same answer, which is essential for a coherent experience and honest A/B data:

import hashlib

def flag_enabled(key, user=None):
    flag = FeatureFlag.objects.filter(key=key).first()
    if not flag or not flag.enabled:
        return False
    if user and flag.allowed_users.filter(pk=user.pk).exists():
        return True
    if flag.rollout_percent >= 100:
        return True
    if not user:
        return False
    bucket = int(hashlib.sha256(
        f"{key}:{user.pk}".encode()).hexdigest(), 16) % 100
    return bucket < flag.rollout_percent

Hashing key:user_id means user 42 stays in the same bucket as you ramp 5% → 25% → 100%, and bucketing differs per flag.

Canary rollout

Ramp gradually while watching error rates and latency: 1% → 5% → 25% → 50% → 100%, pausing at each step. If your dashboards stay green, continue; if they spike, drop back to 0 instantly.

A/B experiments

The same deterministic bucketing powers experiments. Assign users to variant A or B, emit an analytics event with the variant, and measure the metric you actually care about (conversion, retention) — not vanity clicks. Run until you reach statistical significance, then ship the winner and delete the flag.

Kill switches and hygiene

  • Kill switch: wrap risky integrations (a new payment provider, a heavy report) in a flag you can flip off in seconds — no deploy, no rollback.
  • Cache flags in Redis with a short TTL; you don't want a DB hit on every request.
  • Delete stale flags. A flag that's been at 100% for three months is just dead conditional code — remove it. Flag debt is real.

Summary

Feature flags turn scary releases into boring config changes. Build a small deterministic flag layer (hash key:user_id so rollouts are stable), ramp features as canaries while watching metrics, reuse the bucketing for A/B tests, and keep kill switches on anything risky. The discipline that keeps it healthy: cache flag lookups and ruthlessly delete flags once they've fully shipped.