Feature Flags: Release Progresivo y Experimentación Segura
Guía práctica de feature flags: patrones de implementación, rollouts progresivos, kill switches, A/B testing y gestión del ciclo de vida a escala.
Los feature flags desacoplan el despliegue del release: publicas código en producción con la funcionalidad nueva oculta, luego la habilitas para usuarios, regiones o porcentajes específicos — y la desactivas al instante si algo falla, sin redesplegar. Esta guía cubre tipos de flags, una implementación funcional, rollout progresivo con gates de decisión y la disciplina de ciclo de vida que evita que los flags se conviertan en deuda.
Resumen
Cada feature flag crea dos caminos de código donde había uno. Eso te compra seguridad — despliegas el viernes, lanzas el martes — pero cada flag es una bifurcación temporal en tu codebase que alguien tiene que testear, monitorear y eventualmente remover. Los equipos que tratan los flags como andamiaje avanzan más rápido; los que los tratan como arquitectura se ahogan en deuda de flags.
Lo que sigue asume que ya puedes desplegar bajo demanda y que tienes dónde guardar el estado de los flags (una fila de base de datos, un servicio de configuración o una plataforma dedicada).
Una aclaración de entrada: un feature flag no es un sustituto de los tests ni una configuración remota. Los flags son un mecanismo de release — controlan cuál de dos caminos ya testeados corre. Si los usas para evitar testear la rama deshabilitada, o para ajustar constantes que deberían vivir en config, tienes la herramienta equivocada para ambos trabajos.
Cuándo Usarlos
- Desplegar cambios incompletos sin exponerlos a usuarios
- Lanzar gradualmente mientras observas tasas de error y métricas de negocio
- Hacer A/B testing de cambios con tráfico real
- Kill switches de emergencia para rutas riesgosas (pagos, migraciones)
- Mergear a main a diario en lugar de mantener branches de larga duración
Si lo que necesitas es experimentación en lugar de control de release — tests de hipótesis con análisis estadístico — la guía de A/B testing cubre esa capa.
Conceptos Clave
| Concepto | Descripción |
|---|---|
| Feature Flag | Un check condicional que habilita o deshabilita un camino de código |
| Kill Switch | Un flag que deshabilita instantáneamente un cambio en producción |
| Rollout Progresivo | Aumentar gradualmente el porcentaje de usuarios que ven un cambio |
| Flag Dirigido | Un flag habilitado para usuarios, grupos o regiones específicas |
| Contexto de Evaluación | Los atributos (ID de usuario, plan, región) contra los que se evalúa un flag |
| Vida del Flag | El período desde la creación hasta la remoción permanente del código |
| Deuda de Flags | Flags viejos acumulados que saturan código y configuración |
Tipos de Feature Flags
El tipo determina todo lo demás — vida útil, responsable, monitoreo y procedimiento de remoción:
| Tipo | Caso de Uso | Vida |
|---|---|---|
| Flag de release | Ocultar cambios incompletos durante desarrollo | Corta (días a semanas) |
| Flag de experimento | A/B testing y decisiones basadas en datos | Media (semanas a meses) |
| Flag operacional | Circuit breakers, límites de rate, modos debug | Larga (meses a permanente) |
| Flag de permiso | Acceso a capacidades por tier de usuario | Permanente (mejor con RBAC) |
| Kill switch | Deshabilitación de emergencia para cambios riesgosos | Corta (se remueve tras estabilizar) |
Dos reglas derivadas de esa tabla: etiqueta cada flag con su tipo al crearlo — la política de remoción se desprende de ahí — y trata los flags de permiso como un code smell. Los flags que gobiernan acceso de forma permanente son autorización con otro sombrero.
Reglas de Targeting y Contexto de Evaluación
El rollout por porcentaje es solo un eje. Los flags se resuelven contra un contexto de evaluación — los atributos que pasas: ID de usuario, tier de plan, país, dispositivo, versión de la app. Un conjunto de reglas típico evalúa en orden:
- Override de kill — flag apagado para todos gana sobre todo lo demás
- Targets explícitos — IDs de usuario específicos o una allowlist
- Reglas de segmento —
plan = 'enterprise'ocountry = 'DE' - Rollout por porcentaje — bucketing determinista sobre el tráfico restante
- Default — lo que recibe quien no matchea nada
Mantén el contexto pequeño y estable. Cada atributo que añades es una dimensión en la que tus flags pueden diverger — “habilitado para usuarios móviles de la UE en app v3” son tres ejes de sorpresa. Y pasa un identificador de usuario consistente: flags evaluados sobre email en un servicio y user_id en otro producen usuarios que ven ambas experiencias a la vez.
Implementar un Sistema de Flags
Decisión build vs buy:
| Opción | Mejor Para | Ejemplos |
|---|---|---|
| Open-source | Auto-hospedado, control total | Unleash, Flagsmith, Flipt |
| SaaS | Setup rápido, escala gestionada | LaunchDarkly, Split, Optimizely |
| Build propio | Casos simples, integración estrecha | Config en-app + base de datos |
| Archivos de config | Flags estáticos, sin cambios en runtime | Configs YAML/JSON |
Si construyes uno propio, apunta al contrato de OpenFeature — una API de evaluación neutral al proveedor — para que migrar a un proveedor gestionado más adelante no signifique reescribir los call sites.
from dataclasses import dataclass
from typing import Optional
import hashlib
@dataclass
class FeatureFlag:
name: str
enabled: bool
rollout_percentage: float = 100.0
target_users: Optional[list[str]] = None
class FeatureFlagManager:
def __init__(self):
self.flags = {}
def register(self, flag: FeatureFlag):
self.flags[flag.name] = flag
def is_enabled(self, flag_name: str, user_id: str = None) -> bool:
flag = self.flags.get(flag_name)
if not flag or not flag.enabled:
return False
if flag.target_users and user_id:
return user_id in flag.target_users
if flag.rollout_percentage < 100 and user_id:
# Hash determinista: el mismo usuario siempre cae en el mismo bucket
digest = hashlib.md5(f"{flag.name}:{user_id}".encode()).hexdigest()
return int(digest, 16) % 100 < flag.rollout_percentage
return True
ffm = FeatureFlagManager()
ffm.register(FeatureFlag("new-dashboard", enabled=True, rollout_percentage=10))
if ffm.is_enabled("new-dashboard", user_id="user-123"):
show_new_dashboard()
else:
show_legacy_dashboard()
El bucketing con md5 es donde la gente se equivoca. random.random() < 0.10 da una respuesta distinta en cada llamada — el mismo usuario alterna entre la experiencia vieja y la nueva dentro de una misma sesión, y tu rampa del 10 % es en realidad el 10 % de las requests, no de usuarios. Hashear flag.name + user_id hace la asignación determinista, así que subir del 10 % al 50 % mantiene al 10 % original en el grupo de tratamiento.
Versión TypeScript del mismo contrato (para backends o edge workers — usa un hash no criptográfico como FNV-1a o un digest de crypto.subtle, lo importante es el determinismo):
import { createHash } from "node:crypto";
interface FeatureFlag {
enabled: boolean;
rolloutPercentage: number;
targetUsers?: string[];
}
function isEnabled(flagName: string, flag: FeatureFlag | undefined, userId?: string): boolean {
if (!flag?.enabled) return false;
if (flag.targetUsers && userId) return flag.targetUsers.includes(userId);
if (flag.rolloutPercentage < 100 && userId) {
const hash = createHash("md5").update(`${flagName}:${userId}`).digest("hex");
return parseInt(hash, 16) % 100 < flag.rolloutPercentage;
}
return true;
}
Rollout Progresivo con Gates de Decisión
Aumenta la exposición por etapas, y define los criterios para avanzar antes de empezar:
ROLLOUT_STAGES = [
{"name": "dev-team", "percentage": 0, "target_users": ["dev1", "dev2", "qa1"]},
{"name": "1-percent", "percentage": 1, "target_users": None},
{"name": "10-percent", "percentage": 10, "target_users": None},
{"name": "50-percent", "percentage": 50, "target_users": None},
{"name": "full", "percentage": 100, "target_users": None},
]
Una progresión que funciona en la práctica:
- Solo internos — equipo de desarrollo y QA, verificar que la funcionalidad funciona
- Anillo beta — early adopters afines, verificar la UX
- 1 % — detectar crashes y problemas de carga a bajo coste
- 10 % — validar tasas de error y p99 a escala
- 50 % — confirmar que las métricas de negocio se mantienen
- 100 % — luego estabilizar 1-2 semanas antes de remover el flag
Los gates de decisión entre etapas son lo que separa esto de “subir el porcentaje y rezar”. Antes de cada subida verifica: paridad de tasa de error (cohortes habilitado vs deshabilitado), delta de latencia p99 y al menos una métrica de negocio (activación, conversión — la que el flag toque). Si algún gate falla, no haces rollback de un despliegue — volteas el flag, lo cual tarda segundos.
Flags para Migraciones de Base de Datos
Los flags son la mitad en runtime del patrón expand-contract para cambios de esquema. La secuencia segura: añade la columna/tabla nueva, despliega código que escribe en ambas (dual-write) detrás de un flag, backfill, voltea el flag para leer de la ubicación nueva y luego — en un despliegue posterior — remueve el flag y borra la columna vieja.
Cada paso es reversible porque el flag controla qué representación es autoritativa, no el esquema. Si la nueva lectura falla, vuelves atrás; los datos siguen correctos en ambos sitios. Intentar lo mismo solo con despliegues te obliga al fin de semana de “gran migración”.
Kill Switches
Flags que existen solo para deshabilitar una ruta al instante:
def process_payment(order):
if not feature_flags.is_enabled("payment-processing-v2"):
return process_payment_v1(order)
try:
return process_payment_v2(order)
except Exception:
if feature_flags.is_enabled("payment-auto-fallback"):
return process_payment_v1(order)
raise
Pautas que importan: todo cambio riesgoso sale detrás de un kill switch por defecto; los valores de los flags se cachean en proceso para que la evaluación sume microsegundos, no una llamada de red; alguien practica el camino de apagado (un switch que nadie ha volteado nunca es una esperanza, no un control); y el runbook dice qué flags son kill switches y quién puede voltearlos.
Una advertencia sobre el auto-fallback del ejemplo: capturar Exception y reintentar contra v1 puede enmascarar un bug real y cobrar dos veces un pago que se completó a medias. Auto-fallback solo para operaciones seguras de reintentar, y alerta cada vez que se dispare — el fallback silencioso es fallo silencioso.
También conviene decidirlo pronto: quién puede voltear un kill switch. Si voltearlo requiere un despliegue o tres niveles de aprobación, no es un kill switch — es una solicitud de cambio. Dale a la rotación de guardia la capacidad de voltear flags de seguridad sin desplegar, y audita cada volteo para que el log de flags funcione también como línea temporal del incidente.
Monitorear el Rendimiento de los Flags
| Métrica | Por Qué Importa |
|---|---|
| Latencia de evaluación | Los checks de flag suman overhead a cada request |
| Tasa de error por estado | ¿La ruta habilitada es realmente peor? |
| Engagement por cohorte | Compara grupos on/off en la métrica que el flag toca |
| Impacto en conversión | Efecto de negocio del cambio |
| Antigüedad del flag | Los flags vencidos son deuda, no funcionalidad |
panels:
- title: "Tasa de Evaluación de Flags"
query: 'rate(feature_flag_evaluations_total[5m])'
- title: "Kill Switches Activos"
query: 'feature_flag_enabled{name=~".*-kill-switch"}'
- title: "Flags Estancados"
query: 'time() - feature_flag_last_modified > 7776000' # >90 días
El panel de tasa de error por estado se paga solo desde el día uno: convierte “¿el rollout rompió algo?” en un gráfico en lugar de una suposición.
Los Flags de Experimento Necesitan Otra Fontanería
Un flag de experimento no es solo un flag de release con dashboard. Para que los datos signifiquen algo necesitas un evento de exposición — registrar “el usuario X vio la variante Y en el instante T” — porque eso es lo que define tus cohortes. Sin él estás comparando a quien quiso convertir, no a quien fue expuesto.
Dos reglas se desprenden: no reutilices un flag de release como experimento a mitad de camino (el cohorte canary del 1 % contamina tu grupo de test), y mantén la medición del experimento separada del monitoreo del rollout — una responde “¿es seguro?”, la otra “¿es mejor?”.
Ciclo de Vida y Deuda de Flags
Los flags tienen ciclo de vida lo gestiones o no — no gestionarlo solo significa que la deuda se acumula en silencio:
Operativamente:
# Workflow de limpieza
# 1. Listar flags habilitados >30 días sin cambios
# 2. Confirmar que la funcionalidad es estable y está al 100%
# 3. Ticket de remoción — decide el owner del flag, no "alguien"
# 4. Remover el condicional, conservar solo la rama habilitada
# 5. Remover el flag de la config, desplegar, verificar regresiones
Reglas que previenen la deuda: fecha de expiración en cada flag de release y experimento al crearlo (30-60 días); tope de flags activos por servicio — los codebases sanos se mantienen por debajo de ~20-30, y por encima de eso cada matriz de tests y cada revisión de código se complica; archiva los flags removidos en un changelog para que “¿cuándo habilitamos X?” tenga respuesta.
El naming ayuda más de lo que parece. new-dashboard-v2-final no le dice nada a nadie; {area}-{comportamiento}-{ticket} (checkout-retry-logic-4821) hace el flag grepeable, lo ata a un responsable y deja claro qué borrar. Si por el nombre no puedes decir si un flag debería seguir existiendo, probablemente no debería.
Feature Flags y Trunk-Based Development
Los flags son lo que hace seguro mergear trabajo incompleto. En lugar de un feature branch que vive tres semanas y se desvía de main, mergeas piezas pequeñas detrás de un flag deshabilitado — main sigue siendo desplegable, el branch nunca se pudre y el review ocurre en trozos revisables.
La disciplina que lo hace funcionar: el camino deshabilitado tiene que seguir compilando y pasando tests. Mergear código muerto “porque está detrás de un flag” se pudre igual que un branch — cuando por fin lo habilitas, no compila o rompe silenciosamente el camino que lo rodea. CI debería compilar y hacer smoke-test de la ruta con flag aunque esté apagada.
Flags del Servidor vs del Cliente
Dónde se evalúa un flag cambia para qué sirve:
| Aspecto | Lado del servidor | Lado del cliente |
|---|---|---|
| Latencia | Evaluación al inicio del request | Fetch del SDK + evaluación local |
| Exposición del payload | La ruta oculta nunca llega al cliente | El código deshabilitado va en el bundle — y es legible |
| Consistencia | Autoritativa por request | Puede desfasarse hasta que el SDK refresca |
| Bueno para | Pagos, lógica de negocio, experimentos | Variantes de UI, tema, layout |
Dos consecuencias prácticas: nunca pongas una ruta peligrosa solo detrás de un flag del cliente (el código es inspeccionable en el navegador), y para experimentos evalúa del lado del servidor o tendrás flicker — la UI vieja renderizando antes de que cargue el flag.
Llevar el Estado de los Flags a la Aplicación
Cómo llegan los valores a tu código es una decisión de disponibilidad: una llamada remota por evaluación pone al servicio de flags en tu ruta crítica — si cae, todo evalúa al default (por eso importa que el default sea seguro). Patrones comunes, en orden de peso operacional:
- SDK en proceso con polling o streaming — el SDK mantiene una copia local; la evaluación es un lookup de diccionario. La mayoría de setups de producción aterrizan aquí.
- Agente sidecar — un proceso local (como el agente de AWS AppConfig) mantiene el estado y tu app llama a
localhost. Mismo beneficio, amigable a multi-lenguaje. - Evaluación remota — lo más simple para empezar; aceptable en herramientas de poco tráfico, riesgoso como dependencia por request.
Sea cual sea el mecanismo, el estado viejo de los flags es normal — decide cuánto desfase es aceptable. Un kill switch que tarda cinco minutos en propagarse es un “botón de pausa”, que a veces es exactamente lo que necesitas y a veces no sirve.
Un Flag, Un Punto de Decisión
La convención más valiosa: cada flag tiene exactamente un punto de decisión con nombre — un helper tipo features.newDashboard() — llamado desde donde sea necesario. Lo que no haces es esparcir if flags["new-dashboard"] por doce archivos, porque remover el flag se convierte en un problema de recorrido de grafo y la mitad de los call sites no coincide en el default.
# features.py — el único lugar donde viven los nombres de flags
def new_dashboard_enabled(user_id: str) -> bool:
return ffm.is_enabled("new-dashboard", user_id)
Cuando el flag se jubila, borras el check del helper e incorporas la rama True — un solo diff, sin arqueología.
Lo Que Funciona
- Un flag por cambio, nunca condicionales anidados — los flags combinatorios no se pueden testear.
- Default seguro: si el sistema de flags cae, el flag evalúa al comportamiento probado, no al nuevo.
- Evalúa una vez por request, cachea el resultado, propágalo por el contexto — no reevalúes a mitad de request.
- Testea ambos caminos, y haz smoke-test del camino deshabilitado en CI — es el estado que la mayoría de usuarios ve. (La caída de Slack de mayo de 2020 escaló precisamente porque una ruta con flag activo nunca había corrido bajo carga real de producción.)
- Owner, descripción, tipo y expiración en cada flag desde el día uno.
Errores Comunes
- Dejar flags en el código indefinidamente — los flags estancados son ramas muertas, config vieja e interacciones sorpresa. Pasados ~30 días al 100 %, un flag es deuda.
- Bucketing con
Math.random()— experiencia inconsistente por usuario y datos de experimento contaminados; usa hashing determinista. - Checks de flag esparcidos — sin un punto de decisión único, remover un flag exige una expedición por el codebase.
- Flags como control de acceso — el gating de permisos de larga duración pertenece a RBAC, no al store de flags.
- Testear solo el camino habilitado — la rama deshabilitada es lo que producción ejecuta la mayoría del tiempo.
- Interdependencias entre flags — “habilitar B solo si A” crea una matriz de pruebas que nadie verifica; mantén los flags ortogonales.
- Auto-fallback silencioso — las rutas de fallback que no alertan esconden el fallo que debían sobrevivir.
Ver También
- Guía de despliegue canary
- Guía de A/B testing
- Guía de despliegue blue-green
- Feature Toggles — Martin Fowler (taxonomía canónica de tipos de flag)
- Especificación OpenFeature (API de evaluación de flags neutral al proveedor)
Preguntas frecuentes
¿Cuál es la diferencia entre un feature flag y un toggle de configuración?
Vida útil y alcance. Un feature flag es de corta duración, está ligado a un camino de código y se remueve cuando el cambio sale. Un toggle de configuración es un setting operacional de larga duración (nivel de log, límite de rate) que se queda en el sistema. Ambos usan el mismo mecanismo — la diferencia es si alguien planea borrarlo.
¿Cómo manejo feature flags en un sistema distribuido?
Centraliza el estado de los flags en un servicio de flags o config store, evalúa una vez al inicio del request y propaga el resultado por el contexto del request para que los servicios de abajo vean una vista consistente. Sin propagación, el servicio A ve el flag activo y el B lo ve apagado en mitad de un request — así nacen los pedidos procesados a medias.
¿Pueden los feature flags ralentizar mi aplicación?
Sí, si se evalúan de forma ingenua — una llamada remota por check de flag por request suma latencia rápido. Se arregla cacheando los valores en proceso (con TTL corto o push updates), evaluando una sola vez por request y manteniendo los checks fuera de los bucles intensivos.
¿Cuándo debería remover un feature flag?
En cuanto el cambio sea estable y esté al 100 % — típicamente 1-2 semanas tras el rollout completo, y dentro de 30 días como objetivo duro. Si un flag lleva 90 días activo sin tocarse, ya no es un flag; es comportamiento indocumentado.
¿El bucketing de flags debería usar hash o asignación aleatoria?
Hashing determinista de nombre del flag + ID de usuario. La asignación aleatoria da al mismo usuario respuestas distintas entre requests, rompe la consistencia de sesión y corrompe los cohortes de experimento. El hashing hace que "10 % de rollout" sea el 10 % de los usuarios, de forma repetible — y al subir el porcentaje nunca degradas a usuarios ya dentro.
¿Evaluación de flags del lado del servidor o del cliente?
Del servidor para todo lo que sea crítico en corrección o negocio — pagos, precios, experimentos donde el flicker contamina los datos. Del cliente solo para variaciones de UI donde una resolución tardía del flag es inofensiva. Nunca gobiernes una ruta sensible solo con un flag del cliente: el código deshabilitado igualmente sale en el bundle.
¿Cuántos flags son demasiados?
No hay número duro, pero ~20-30 flags activos por servicio es un techo razonable — por encima, testear ambos estados de cada flag se vuelve inmanejable y nadie sabe qué hace la mitad de ellos. Si estás por encima, la solución es disciplina de ciclo de vida (expiración al crear, revisión mensual), no un store de flags más grande.
Recursos Relacionados
Estrategias de Despliegue: Blue-Green, Canary y Rolling
Guía práctica de estrategias de despliegue: rolling, blue-green, canary, shadow y feature flags. Minimiza el riesgo y el tiempo de rollback al liberar a producción.
GuideDespliegue Canary: Rollouts Graduales y Seguros
Guía práctica de despliegues canary: división de tráfico, promoción automatizada, disparadores de rollback y releases seguros a un subconjunto de usuarios.
GuideGuía de A/B Testing: Diseño, Estadística y Errores
Guía práctica de A/B testing: diseño de hipótesis, tamaño de muestra, análisis estadístico, herramientas y fallos que invalidan experimentos.
GuideIngenieria de confiabilidad del sitio (SRE)
Guia practica de SRE: definir SLIs, SLOs y SLAs, gestionar presupuestos de error, reducir toil, rotaciones de guardia y construir una cultura de confiabilidad.
GuideGuía Completa de Observabilidad con el Stack de Grafana
Monta el stack completo de observabilidad de Grafana: Prometheus, Loki, Tempo, Alloy y Grafana con un solo docker-compose, correlación de señales y alertado por SLO.
GuideGuía de Despliegue Blue-Green
Guía práctica de despliegues blue-green: decisiones de arquitectura, mecanismos de conmutación de tráfico, migraciones de base de datos, planificación de rollback y operaciones de release sin interrupción.