Feature flags decouple deployment from release. Ship code anytime, enable features when ready, roll back without deploying.
February 23, 2026 · 7 min · 1351 words · Rob Washington
Table of Contents
Deployment and release are different things. Deployment puts code on servers. Release makes features available to users. Feature flags separate these concerns.
With feature flags, you can deploy daily but release weekly. Ship risky changes but only enable them for 1% of users. Roll back a broken feature in seconds without touching infrastructure.
classFeatureFlags:def__init__(self):self.flags={}defis_enabled(self,flag_name:str,user_id:str=None)->bool:flag=self.flags.get(flag_name)ifnotflag:returnFalse# Global kill switchifnotflag.get("enabled",False):returnFalse# Check user allowlistifuser_idanduser_idinflag.get("allowed_users",[]):returnTrue# Check percentage rolloutifuser_idandflag.get("percentage",0)>0:# Consistent hashing: same user always gets same resulthash_val=hash(f"{flag_name}:{user_id}")%100returnhash_val<flag["percentage"]returnflag.get("enabled",False)flags=FeatureFlags()flags.flags={"new_checkout":{"enabled":True,"percentage":10,# 10% of users"allowed_users":["user_123"]# Plus specific users}}# Usageifflags.is_enabled("new_checkout",user_id=current_user.id):returnnew_checkout_flow()else:returnold_checkout_flow()
importyamlclassFeatureFlags:def__init__(self,config_path:str):withopen(config_path)asf:self.config=yaml.safe_load(f)self.flags=self.config.get("flags",{})defreload(self):"""Hot reload without restart"""withopen(self.config_path)asf:self.config=yaml.safe_load(f)self.flags=self.config.get("flags",{})
defrollout_schedule(flag_name:str):"""
Day 1: 1% (canary)
Day 2: 10%
Day 3: 25%
Day 4: 50%
Day 5: 100%
"""pass# Consistent user bucketingimporthashlibdefget_bucket(user_id:str,flag_name:str)->int:"""Returns 0-99, consistent for same user+flag"""key=f"{flag_name}:{user_id}"hash_bytes=hashlib.md5(key.encode()).digest()returnint.from_bytes(hash_bytes[:2],'big')%100defis_in_rollout(user_id:str,flag_name:str,percentage:int)->bool:returnget_bucket(user_id,flag_name)<percentage
fromdatetimeimportdatetime,timedeltaclassFlagWithMetadata:def__init__(self,name:str):self.name=nameself.created_at=datetime.now()self.last_checked=Noneself.check_count=0defis_stale(self,days:int=90)->bool:returndatetime.now()-self.created_at>timedelta(days=days)# Alert on stale flagsforflaginall_flags:ifflag.is_stale()andflag.percentage==100:alert(f"Flag '{flag.name}' is fully rolled out and over 90 days old. Remove it.")
importpytestfromunittest.mockimportpatchdeftest_new_checkout_enabled():withpatch.object(flags,'is_enabled',return_value=True):response=client.post("/checkout")assertresponse.json()["flow"]=="new"deftest_new_checkout_disabled():withpatch.object(flags,'is_enabled',return_value=False):response=client.post("/checkout")assertresponse.json()["flow"]=="old"# Or use flag overrides in test config@pytest.fixturedeffeature_flags():returnFeatureFlags(config={"new_checkout":{"enabled":True,"percentage":100}})
fromprometheus_clientimportCounterflag_evaluations=Counter('feature_flag_evaluations_total','Feature flag evaluations',['flag_name','result'])defis_enabled(flag_name:str,user_id:str)->bool:result=_evaluate_flag(flag_name,user_id)flag_evaluations.labels(flag_name,str(result)).inc()returnresult
# ❌ Flag from 2 years ago, nobody knows if it's safe to removeifflags.is_enabled("new_user_flow_2022"):...
Flags without owners:
1
2
3
4
5
6
# ✅ Good: Include ownershipnew_checkout:enabled:trueowner:"payments-team"expires:"2024-06-01"jira:"PAY-1234"
Feature flags transform deployment from a high-stakes event into a routine operation. Ship code continuously. Release features deliberately. Roll back instantly.
Start simple: boolean flags with percentage rollouts. Graduate to targeting rules when needed. Use a service when flag management becomes its own job. Clean up flags aggressively.
The best feature flag is the one you remember to delete.
📬 Get the Newsletter
Weekly insights on DevOps, automation, and CLI mastery. No spam, unsubscribe anytime.