Feature flags: despliega código generado por IA sin miedo
El 1 de agosto de 2012, Knight Capital acumuló en unos 45 minutos posiciones que acabaron costándole más de 460 millones de dólares. No fue un ataque ni un fallo de hardware. Fueron un despliegue incompleto y un feature flag mal gestionado: se reutilizó para activar código nuevo y, en el único servidor que no recibió ese código, despertó código que no se usaba desde 2003.
Lo peor vino después: para arreglarlo, retiraron el código nuevo de los servidores donde sí funcionaba. Eso empeoró el problema.
Hoy un agente te abre un PR de 900 líneas de código generado por IA en diez minutos. Los tests pasan. Y aun así no sabes cómo se comportará con tráfico real.
En corto: mergea el código generado por IA a main detrás de un feature flag apagado por defecto, actívalo primero para tu equipo y luego por porcentaje de usuarios con un hash estable del userId. Si algo falla, apagas el flag en vez de hacer revert y redeploy. Y borras el flag y el camino viejo en cuanto llega al 100 %: a Knight lo hundieron un flag reutilizado, código muerto que nadie borró y un despliegue manual que nadie revisó.
¿Qué es un feature flag?
Un feature flag es una condición en el código cuyo valor se decide en tiempo de ejecución, fuera del despliegue, y que permite activar o desactivar un comportamiento sin cambiar ni volver a desplegar el código.
La clave es separar desplegar de liberar. Desplegar es poner el código en producción; liberar es que los usuarios lo ejecuten. Con un flag, lo primero lo hace tu pipeline y lo segundo lo decides tú.
Pete Hodgson los clasifica en cuatro tipos en Feature Toggles, el artículo de referencia en martinfowler.com:
| Tipo | Para qué sirve | Vida esperada |
|---|---|---|
| Release | Ocultar código sin validar en producción | Días o semanas; luego se borra |
| Ops (kill switch) | Apagar una función cara o frágil cuando el sistema sufre | Casi siempre corta; algunos kill switches, años |
| Experimento | Comparar variantes A/B con métricas | Lo que dure el experimento |
| Permiso | Dar acceso a planes premium o usuarios beta | Años |
Para el código generado por IA interesa el de release, y Hodgson es tajante con su duración: "They should generally not stick around much longer than a week or two".
Por qué el código generado por IA necesita feature flags
El cuello de botella ya no es escribir código, es confiar en él. Un agente produce cambios más rápido de lo que tu equipo los valida, y hay comportamientos que solo descubres con tráfico real.
Sin flags tienes dos salidas malas: una rama larga que acaba en un merge infernal, o liberar a todo el mundo a la vez. Ninguna escala cuando el agente abre cinco PR al día.
Es el argumento de trunk-based development que vimos en cómo acelerar las entregas de los desarrolladores sénior: integrar pronto y en trozos pequeños. El flag hace seguro integrar código que aún no quieres que vea nadie.
| Criterio | Revert + redeploy | Feature flag | Rama larga |
|---|---|---|---|
| Cómo apagas el cambio | Nuevo commit y pipeline completo | Cambias un valor en el proveedor de flags | No hace falta: nunca llegó a main |
| Tiempo hasta apagar | Lo que tarde tu CI/CD | Lo que tarde en propagarse la configuración | No aplica |
| Exposición al activar | 100 % de usuarios de golpe | Equipo, luego 5 %, 25 %, 100 % | 100 % de golpe al mergear |
| Conflictos de merge | Bajos | Bajos | Altos, crecen cada día |
| Limitación / riesgo | El revert puede no ser limpio si hubo migraciones o el cambio tocó varios servicios | Deuda de flags, combinatoria de estados en tests, dependencia del proveedor | El código no se prueba con tráfico real hasta el final; integración tardía |
El revert no siempre es la red de seguridad que parece: en Knight Capital, retirar el código nuevo activó el defectuoso en siete servidores más.
Feature flags en TypeScript con OpenFeature
OpenFeature es la forma de escribir feature flags en TypeScript sin casarte con un proveedor. Es una especificación abierta que la CNCF aceptó como proyecto incubating en diciembre de 2023: tu código habla con el SDK y el proveedor se enchufa detrás.
El ejemplo está ejecutado con Node.js 24.16.0, @openfeature/server-sdk 1.23.0, @openfeature/core 1.12.0, tsx 4.23.15 y TypeScript 7.0.2 (tsc --noEmit sin errores). Usa TypedInMemoryProvider, porque en esta versión InMemoryProvider está marcado como @deprecated.
Primero, el rollout por porcentaje. Math.random() < 0.05 es un error: el mismo usuario vería la función nueva en una petición y la vieja en la siguiente. Necesitas un hash estable.
// rollout.ts
import { createHash } from 39;node:crypto39;;
// Devuelve un "bucket" estable entre 0 y 99 para un usuario y un flag.
// Mismo userId + mismo flag = mismo bucket, en cualquier proceso y en cualquier máquina.
export function bucketOf(flagKey: string, userId: string): number {
const digest = createHash(39;sha25639;).update(`${flagKey}:${userId}`).digest();
return digest.readUInt32BE(0) % 100;
}
export function isInRollout(flagKey: string, userId: string, percentage: number): boolean {
return bucketOf(flagKey, userId) < percentage;
}
El nombre del flag entra en el hash a propósito: si no, el mismo 5 % de usuarios se comería todos tus rollouts.
Ahora el flag: siempre activo para el equipo, luego por porcentaje, y disabled: true como kill switch:
// flags.ts
import { OpenFeature, TypedInMemoryProvider, type EvaluationContext } from 39;@openfeature/server-sdk39;;
import { isInRollout } from 39;./rollout39;;
const FLAG = 39;new-checkout-ai39;;
const TEAM = new Set([39;ana@empresa.dev39;, 39;luis@empresa.dev39;]);
// Construye la configuración del flag: equipo interno + porcentaje + kill switch
function buildFlags(percentage: number, killed: boolean) {
return {
[FLAG]: {
variants: { on: true, off: false },
defaultVariant: 39;off39;,
disabled: killed,
contextEvaluator: (ctx: EvaluationContext) => {
const userId = ctx.targetingKey;
if (!userId) return 39;off39;;
if (TEAM.has(String(ctx.email))) return 39;on39;;
return isInRollout(FLAG, userId, percentage) ? 39;on39; : 39;off39;;
},
},
} as const;
}
const provider = new TypedInMemoryProvider(buildFlags(0, false));
await OpenFeature.setProviderAndWait(provider);
const client = OpenFeature.getClient();
// Valor por defecto seguro: si algo falla, el código nuevo NO se ejecuta
const SAFE_DEFAULT = false;
async function share(): Promise<number> {
let on = 0;
for (let i = 0; i < 10_000; i++) {
if (await client.getBooleanValue(FLAG, SAFE_DEFAULT, { targetingKey: `user-${i}` })) on++;
}
return on / 100;
}
console.log(39;1) Merge con el flag al 0 %39;);
console.log(39; equipo (ana):39;, await client.getBooleanValue(FLAG, SAFE_DEFAULT, { targetingKey: 39;u-ana39;, email: 39;ana@empresa.dev39; }));
console.log(39; usuario externo:39;, await client.getBooleanValue(FLAG, SAFE_DEFAULT, { targetingKey: 39;user-4239; }));
console.log(` % de 10.000 usuarios con el flag: ${await share()} %`);
provider.putConfiguration(buildFlags(5, false));
const firstWave = new Set<string>();
for (let i = 0; i < 10_000; i++) {
if (await client.getBooleanValue(FLAG, SAFE_DEFAULT, { targetingKey: `user-${i}` })) firstWave.add(`user-${i}`);
}
console.log(39;2) Rollout al 5 %39;);
console.log(` % real: ${firstWave.size / 100} %`);
const again = await client.getBooleanValue(FLAG, SAFE_DEFAULT, { targetingKey: [...firstWave][0] });
console.log(` ${[...firstWave][0]} vuelve a evaluar -> ${again} (estable)`);
provider.putConfiguration(buildFlags(25, false));
let kept = 0;
for (const id of firstWave) if (await client.getBooleanValue(FLAG, SAFE_DEFAULT, { targetingKey: id })) kept++;
console.log(39;3) Rollout al 25 %39;);
console.log(` % real: ${await share()} %`);
console.log(` usuarios del 5 % que siguen dentro: ${kept}/${firstWave.size}`);
provider.putConfiguration(buildFlags(25, true));
const details = await client.getBooleanDetails(FLAG, SAFE_DEFAULT, { targetingKey: [...firstWave][0], email: 39;ana@empresa.dev39; });
console.log(39;4) Kill switch (disabled: true)39;);
console.log(` value=${details.value} reason=${details.reason}`);
console.log(` % real: ${await share()} %`);
const missing = await client.getBooleanDetails(39;flag-que-no-existe39;, SAFE_DEFAULT, { targetingKey: 39;user-139; });
console.log(39;5) Flag inexistente39;);
console.log(` value=${missing.value} reason=${missing.reason} errorCode=${missing.errorCode}`);
Salida real de npx tsx flags.ts:
1) Merge con el flag al 0 %
equipo (ana): true
usuario externo: false
% de 10.000 usuarios con el flag: 0 %
2) Rollout al 5 %
% real: 5.27 %
user-0 vuelve a evaluar -> true (estable)
3) Rollout al 25 %
% real: 24.98 %
usuarios del 5 % que siguen dentro: 527/527
4) Kill switch (disabled: true)
value=false reason=DISABLED
% real: 0 %
5) Flag inexistente
value=false reason=ERROR errorCode=FLAG_NOT_FOUND
Al subir del 5 % al 25 %, los 527 usuarios de la primera ola siguen dentro: nadie ve la función aparecer y desaparecer. El kill switch gana incluso al equipo interno (reason=DISABLED).
Y si el flag no existe, el SDK no lanza: devuelve el valor por defecto con errorCode=FLAG_NOT_FOUND. Por eso el default es false, el camino viejo que ya funcionaba.
Rollout gradual de código generado por IA: el flujo con agentes
El rollout gradual convierte "lo despliego y rezo" en una serie de pasos pequeños con un botón de apagado en cada uno. Con agentes, el flujo queda así:
- El agente abre el PR con el código nuevo detrás de un flag apagado.
- Lo revisas igual: el código está en
main. Aquí encaja la revisión por contrato. - Mergeas el mismo día y tu pipeline de CI/CD despliega.
- Lo activas para el equipo interno.
- Subes a 5 %, 25 %, 50 % y 100 %, mirando métricas en cada paso.
- Estable al 100 %, el agente abre otro PR que borra el flag y el camino viejo.
El paso 5 solo funciona si mides. Sin monitoreo en producción que separe métricas por variante, subir del 5 % al 25 % es fe, no datos.
El paso 1 hay que pedirlo de forma explícita. Una regla en tu AGENTS.md como "todo cambio de comportamiento va detrás de un flag nuevo, apagado por defecto, con fecha de borrado" hace que ocurra siempre. Montar esas reglas y el harness del agente es lo que se trabaja en el workshop SDD + Agentic Engineering.
El curso Construye con IA recorre el proceso completo, de la idea al producto desplegado con Claude Code.
Lo que Knight Capital enseña sobre feature flags
La lección no es "no uses flags". Es: nunca reutilices el nombre de un flag, borra el código muerto y no confíes en que el rollback sea inocuo. La orden de la SEC de octubre de 2013 lo documenta:
- El código nuevo "repurposed a flag that was formerly used to activate the Power Peg code", sin uso desde 2003.
- Un técnico no copió el código nuevo a uno de los ocho servidores, y nadie revisó el despliegue.
- Ese servidor ejecutó el código viejo: 4 millones de ejecuciones en unos 45 minutos.
- Desinstalar el código nuevo de los otros siete extendió el fallo a todos.
En el hilo de Hacker News It's OK to hardcode feature flags, un usuario cuenta que una función nueva paralizó su app y una de las razones por las que tardaron tanto en arreglarlo fue que el responsable había quitado el flag remoto ese mismo día: "Redeploying takes time. Sometimes you want to disable something quick."
En el mismo hilo, otro comentario resume el reverso: "leaving too many feature flags lying around can become serious debt". Las dos cosas son ciertas a la vez.
Límites de los feature flags
Los feature flags reducen el riesgo de liberar código, pero tienen su propio coste:
Deuda de flags. Cada flag es un if con dos caminos que mantener; Hodgson habla de inventario con "carrying cost". Sin fecha de borrado se quedan para siempre: deuda técnica que además confunde al agente.
Combinatoria en los tests. Con 10 flags booleanos activos hay 1.024 combinaciones. Testea los dos estados de cada flag y mantén pocos vivos a la vez.
Dónde se evalúa. Si evalúas en el navegador, el código nuevo viaja en el bundle aunque el flag esté apagado. Cualquiera puede leerlo. Para funciones sensibles, evalúa en servidor o elimina el código en build.
Latencia y caída del proveedor. Un proveedor remoto es una dependencia más. Si cae y no tiene caché, recibes el valor por defecto, y el kill switch tarda lo que tarde tu proveedor en propagar cambios.
Los flags no sustituyen a los tests. Un flag limita a cuántos usuarios afecta un fallo, no evita el fallo. Las migraciones de base de datos tampoco se apagan con un flag: tienen que ser compatibles con los dos caminos.
Por dónde empezar con feature flags
Empieza por un solo flag de release en el próximo PR que te abra un agente. No necesitas un SaaS: OpenFeature con el provider en memoria para los tests y un provider que lea configuración externa en producción basta para separar desplegar de liberar.
Ponle fecha de borrado el mismo día que lo creas: es lo que separa un flag útil de un Knight Capital en miniatura. Y si quieres que el agente parta de una especificación clara, el libro de Spec-Driven Development explica cómo definirla antes de escribir una línea.
Preguntas frecuentes
¿Por qué no usar Math.random() para un rollout por porcentaje?
Porque cada evaluación daría un resultado distinto y el mismo usuario vería la función nueva y la vieja alternándose. Un hash estable del userId y del nombre del flag asigna a cada usuario siempre el mismo bucket, y al subir el porcentaje los que ya estaban dentro siguen dentro.
¿Qué pasa si el proveedor de feature flags se cae?
Con OpenFeature, la evaluación no lanza una excepción: si el provider no puede resolver el flag, devuelve el valor por defecto que pasaste en la llamada, con reason=ERROR y un errorCode. Muchos providers remotos sirven antes el último valor cacheado. Por eso el valor por defecto debe ser siempre el comportamiento antiguo y probado.
¿Qué diferencia hay entre un feature flag de release y un kill switch?
Un flag de release oculta código nuevo mientras lo validas y se borra en una o dos semanas. Un kill switch es un flag de operaciones que sirve para apagar una función cara o frágil cuando el sistema sufre; casi siempre es temporal, aunque algunos se quedan años. En OpenFeature, disabled: true devuelve el valor por defecto con reason=DISABLED, incluso para el equipo interno.
¿Cuánto tiempo debe vivir un flag de release?
Pete Hodgson recomienda que no pase de una o dos semanas. Al 100 % y estable, borra el flag y el camino viejo en un PR aparte.
¿Necesito LaunchDarkly o un SaaS para usar feature flags?
No para empezar. Con OpenFeature escribes contra una API estándar y, si luego necesitas segmentación o auditoría, cambias de provider sin tocar las llamadas.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
¿Te resultó útil este artículo?
Compártelo con tu comunidad y ayuda a otros desarrolladores.
