Módulo 5: Rollback And Incident Response
Rollback vs. fix-forward: la decisión completa
Descripción
La lección 2 midió el costo de revertir recommendations: 2 minutos, gracias al kill switch. Pero un número solo no es una decisión — hace falta compararlo contra algo. Esta lección construye esa comparación completa con rollbackDecision(): un modelo que recibe el estado de los guardrails, el resultado del experimento, y el costo estimado de cada camino —revertir, o arreglar hacia adelante— y devuelve una de tres respuestas: continue, rollback, o fix_forward.
Conexión con el módulo. Esta es la pieza central del módulo: el modelo que la lección 1 prometió y que el proyecto de la lección 8 va a ejecutar sobre el incidente completo de recommendations. Todo lo que viene después —el runbook (L4), mitigar antes de diagnosticar (L5), la severidad (L6)— asume que ya se tomó esta decisión, o ayuda a tomarla más rápido. Esta lección es donde se toma.
Una analogía: ¿regresar por donde viniste, o seguir hasta la salida más cercana?
Imagina que vas manejando por una carretera de montaña y, a mitad de camino, notas que el auto empieza a fallar. Tienes dos opciones, y ninguna es automáticamente la correcta: regresar por el mismo camino que ya conoces, hacia el punto de partida — una ruta segura, ya probada, pero que puede significar dar toda la vuelta de nuevo. O seguir adelante hasta la salida más cercana que ya está a la vista, más corta en este momento específico, pero por un tramo de carretera todavía no confirmado. La decisión correcta no depende de una regla fija ("siempre regresa" o "siempre sigue adelante") — depende de qué tan lejos estás de cada punto, y qué tan confiable es el auto para llegar a cualquiera de los dos.
rollback es "regresar por donde viniste": volver al estado de antes, ya conocido, ya probado — en esta guía, apagar recommendations con el kill switch. fix-forward es "seguir hasta la salida más cercana": no deshacer el cambio, sino arreglar el problema específico manteniendo la feature activa. Ninguno de los dos es la respuesta correcta por default. La respuesta correcta depende de cuál de los dos caminos es, en ese momento específico, más rápido y más seguro — exactamente lo que rollbackDecision() calcula.
Ejemplo trabajado: rollbackDecision() sobre tres escenarios
// rollbackDecision: dado el estado de los guardrails y el costo estimado de revertir
// vs arreglar hacia adelante, decide el siguiente paso. primary.win NUNCA anula un
// guardrail critico roto -- ese es el error que esta leccion nombra.
function rollbackDecision({ guardrails, primary, revertCost, fixForwardCost }) {
const brokenCritical = guardrails.filter((g) => g.critical && g.broken);
if (brokenCritical.length === 0) {
return 'continue'; // ningun guardrail critico roto -- el resultado del primario manda
}
return revertCost <= fixForwardCost ? 'rollback' : 'fix_forward';
}
const scenarios = [
{
label: 'recommendations, rollout 10% (el caso real de esta guia)',
guardrails: [
{ name: 'p95Latency', critical: true, broken: true, value: 910, ceiling: 800 },
{ name: 'checkoutErrorRate', critical: true, broken: false, value: 0.006, ceiling: 0.02 },
],
primary: { name: 'checkoutConversion', win: true, lift: 0.1875 },
revertCost: 2, // minutos, killSwitch (leccion 2)
fixForwardCost: 180, // minutos: diagnosticar la query lenta + parchear + verificar
},
{
label: 'recommendations, si NINGUN guardrail critico se hubiera roto',
guardrails: [
{ name: 'p95Latency', critical: true, broken: false, value: 760, ceiling: 800 },
{ name: 'checkoutErrorRate', critical: true, broken: false, value: 0.006, ceiling: 0.02 },
],
primary: { name: 'checkoutConversion', win: true, lift: 0.1875 },
revertCost: 2,
fixForwardCost: 180,
},
{
label: 'checkoutRedesign: el flag tambien protege un fix de pagos ya en produccion',
guardrails: [
{ name: 'paymentSuccessRate', critical: true, broken: true, value: 0.94, ceiling: 0.98 },
],
primary: { name: 'checkoutCompletionTime', win: true, lift: 0.08 },
revertCost: 400, // revertir el flag tambien revierte un fix de seguridad de pagos ya validado
fixForwardCost: 45, // parche acotado solo al bug que rompio paymentSuccessRate
},
];
console.log('=== rollbackDecision sobre tres escenarios ===\n');
scenarios.forEach((s) => {
const decision = rollbackDecision(s);
console.log(s.label);
console.log(' primary.win=' + s.primary.win + ' revertCost=' + s.revertCost + 'min fixForwardCost=' + s.fixForwardCost + 'min');
console.log(' -> decision: ' + decision + '\n');
});
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== rollbackDecision sobre tres escenarios ===
recommendations, rollout 10% (el caso real de esta guia)
primary.win=true revertCost=2min fixForwardCost=180min
-> decision: rollback
recommendations, si NINGUN guardrail critico se hubiera roto
primary.win=true revertCost=2min fixForwardCost=180min
-> decision: continue
checkoutRedesign: el flag tambien protege un fix de pagos ya en produccion
primary.win=true revertCost=400min fixForwardCost=45min
-> decision: fix_forward
Los tres escenarios prueban las tres ramas del modelo, y vale la pena leerlos uno por uno. En el primero, el caso real de esta guía, p95Latency está roto y el kill switch cuesta apenas 2 minutos contra los 180 minutos que tomaría diagnosticar y parchar la query lenta con recommendations todavía activa — la decisión es rollback, sin que el primary.win=true haya influido en absoluto. En el segundo, con los mismos costos pero sin ningún guardrail crítico roto, la función ni siquiera llega a comparar revertCost contra fixForwardCost — devuelve continue de inmediato, porque no hay nada que revertir. En el tercero, con un flag distinto (checkoutRedesign) que también protege un fix de seguridad de pagos ya validado, revertirlo por completo tiene un costo mucho más alto (400 minutos, porque también habría que volver a lanzar ese fix después) que un parche acotado solo al bug nuevo (45 minutos) — ahí, y solo ahí, la decisión es fix_forward.
Por qué primary.win nunca aparece en la comparación
Vuelve al cuerpo de rollbackDecision(): el parámetro primary se recibe, se usa en los console.log() para mostrar contexto — pero nunca entra en la lógica que decide el resultado. La función pregunta, primero, si hay algún guardrail crítico roto. Si la respuesta es no, listo, continue. Si la respuesta es sí, la única comparación que sigue es revertCost contra fixForwardCost — el resultado del experimento no vuelve a aparecer en ningún punto de esa comparación.
Esto no es un descuido — es la corrección deliberada del error que la lección 1 ya nombró: "el A/B ganó" y "el guardrail está roto" son dos hechos independientes, y mezclarlos en una sola decisión lleva, casi siempre, a justificar seguir adelante con algo que ya se sabe que está rompiendo un límite que alguien, con cuidado, decidió que no debía cruzarse. Si quisieras que primary.win influyera en algo, tendrías que preguntarte explícitamente: ¿existe alguna versión razonable de esta lógica donde un resultado positivo grande justifique tolerar un guardrail roto por más tiempo? La respuesta, en el marco de esta guía, es no — un guardrail crítico es, por definición, una línea que no se negocia con el resultado del primario. Si esa línea puede negociarse, no debería llamarse "crítica" en primer lugar.
Errores comunes
Ignorar la primera condición y saltar directo a comparar costos. Qué pasa: alguien, apurado, calcula revertCost y fixForwardCost para cualquier situación, sin verificar primero si de verdad hay un guardrail crítico roto — y termina "decidiendo" entre rollback y fix-forward para un caso donde la respuesta correcta era simplemente continue. Por qué pasa: comparar dos números se siente como el corazón del problema, y el chequeo inicial —"¿hay algo roto?"— puede sentirse como un paso trivial que no hace falta verificar con cuidado. Cómo detectarlo: si en algún momento estás debatiendo si revertir o no revertir, y nadie puede nombrar cuál guardrail crítico está roto en ese momento, probablemente no debería haber debate — la respuesta debería ser continue. Cómo corregirlo: como hace rollbackDecision(), siempre evalúa primero si existe un guardrail crítico roto, antes de gastar tiempo estimando costos de reversión que, si no hay nada roto, no hacen falta.
Estimar fixForwardCost de forma optimista, sin incluir el tiempo de verificación. Qué pasa: alguien calcula el costo de un fix hacia adelante contando solo el tiempo de escribir el parche —"esto lo arreglo en 20 minutos"—, sin sumar el tiempo real de probarlo, desplegarlo, y confirmar que de verdad resolvió el guardrail sin romper nada más. Por qué pasa: escribir la línea de código que arregla el bug se siente como "el trabajo", y todo lo demás —pruebas, despliegue, verificación— se siente como trámite que se puede subestimar bajo presión. Cómo detectarlo: si el fixForwardCost estimado no incluye, explícitamente, tiempo de verificación después del despliegue, el número está incompleto — y probablemente más bajo de lo que sería en la realidad. Cómo corregirlo: como en el ejemplo de checkoutRedesign de esta lección, el costo de un fix hacia adelante incluye diagnosticar, parchar, desplegar, y verificar que el guardrail volvió a estar dentro del techo — cuatro pasos, no uno.
Tratar revertCost como si fuera siempre 2 minutos, sin importar el caso. Qué pasa: alguien memoriza el número de la lección 2 —el kill switch cuesta 2 minutos— y lo aplica a cualquier situación, sin preguntarse si esta vez el flag protege algo más que la feature en cuestión, como pasó con checkoutRedesign en el tercer escenario de esta lección. Por qué pasa: 2 minutos es un número tan memorable, y tan consistentemente bajo en los ejemplos anteriores, que se siente como una constante universal en vez de un dato que depende del contexto específico de cada flag. Cómo detectarlo: la pregunta correctora es "¿este flag protege solo la feature que quiero revertir, o también algo más que perdería valor si lo apago?" Si la respuesta es "también algo más", el revertCost real es mayor que el costo mecánico de apagar un booleano. Cómo corregirlo: como en el tercer escenario de esta lección, calcula revertCost caso por caso — el costo mecánico del kill switch (segundos) es solo el piso; el costo real puede ser mayor si revertir tiene efectos secundarios.
Ejercicios
Ejercicio 1 — Cambia un solo número. En el primer escenario (recommendations, rollout 10%), ¿qué pasaría con la decisión si fixForwardCost bajara de 180 a 1 minuto (imagina que el fix es, literalmente, cambiar un solo valor de configuración, sin necesidad de desplegar código)? Ejecuta mentalmente rollbackDecision() con ese cambio y explica el resultado.
Ver solución
Con fixForwardCost: 1 y revertCost: 2 sin cambios, la comparación revertCost <= fixForwardCost se vuelve 2 <= 1, que es false — así que la decisión pasa a ser fix_forward. Esto es coherente con el principio de la lección: cuando arreglar hacia adelante es más rápido que revertir, y sigue siendo seguro, es la opción que gana. El kill switch no es la respuesta correcta por dogma — es la respuesta correcta en el caso real de esta guía porque, en ese caso específico, es la opción más rápida disponible.
Ejercicio 2 — Diseña un cuarto escenario. Escribe un objeto de escenario, siguiendo el mismo formato que los tres de esta lección, para un caso donde ningún guardrail crítico está roto pero uno no crítico sí lo está (por ejemplo, { name: 'pageLoadAnimationSmoothness', critical: false, broken: true }). ¿Qué decisión da rollbackDecision(), y por qué el resultado es coherente con cómo está escrito el filtro guardrails.filter((g) => g.critical && g.broken)?
Ver solución
La decisión sería continue, sin importar qué tan roto esté el guardrail no crítico. El filtro g.critical && g.broken exige que ambas condiciones sean verdaderas para que un guardrail cuente como "roto crítico" — un guardrail con broken: true pero critical: false no pasa ese filtro, así que brokenCritical.length sigue siendo 0, y la función nunca llega a comparar revertCost contra fixForwardCost. Esto refleja una idea intencional: no todos los guardrails detienen un lanzamiento de la misma forma — los guardrails no críticos son señales a vigilar (posiblemente para un fix futuro, sin urgencia), no disparadores automáticos de una decisión de rollback.
Ejercicio 3 — Explica la decisión sin usar "rollback" ni "fix-forward". En 2-3 frases, explica a alguien de negocio por qué Mercado decidió apagar recommendations en vez de intentar arreglar la latencia mientras seguía activa, sin usar ninguno de los dos términos técnicos.
Ver solución
Un ejemplo de respuesta: "Cuando confirmamos que la latencia estaba por encima del límite que habíamos fijado de antemano, comparamos dos caminos: apagar la funcionalidad por completo, que toma un par de minutos y no afecta nada más del sitio, contra investigar y arreglar el problema específico mientras se mantiene activa, que hubiera tomado varias horas. Con esa diferencia de tiempo, y sabiendo que apagarla no rompe nada del resto de la plataforma, la decisión más segura para nuestros compradores fue apagarla primero e investigar después, sin presión." La idea central: la decisión se explica por la comparación de costos y riesgo, no por ningún principio abstracto sobre qué "se supone" que hay que hacer.
Resumen y siguiente paso
En esta lección construiste rollbackDecision(), el modelo central de este módulo: compara el estado de los guardrails críticos contra el costo de revertir y el costo de arreglar hacia adelante, y devuelve continue, rollback, o fix_forward — sin que el resultado del experimento primario influya nunca en esa comparación. Sobre el caso real de recommendations, con un kill switch de 2 minutos contra un fix estimado en 180, la decisión es clara: rollback.
Antes de avanzar deberías poder: explicar por qué primary.win no aparece en la lógica de decisión; calcular rollbackDecision() a mano para un escenario nuevo, dados sus guardrails y costos; y nombrar una situación realista donde fix_forward sea la decisión correcta, no rollback.
Ya sabes qué decisión tomar. La lección 4 contesta la pregunta que sigue de inmediato: una vez que el equipo decide "rollback", ¿cómo se ejecuta esa decisión sin que cada persona improvise su propio orden de pasos en medio de la presión de un incidente en vivo?
Recursos
- Charity Majors, "Deploys Are the WRONG Way to Change User Experience" — honeycomb.io/blog/deploys-wrong-way-change-user-experience. Argumenta que revertir con un feature flag es casi siempre más rápido y más seguro que un rollback de código — la base directa de por qué
revertCostes tan bajo en el caso derecommendations. En inglés. - Google SRE Book, Capítulo 14, "Managing Incidents" — sre.google/sre-book/managing-incidents. Describe la disciplina de tomar decisiones rápidas y documentadas durante un incidente, en vez de debatir sin estructura — el espíritu detrás de tener un modelo explícito como
rollbackDecision()en vez de discutir cada vez desde cero. En inglés. - Atlassian, "How to choose incident management KPIs and metrics" — atlassian.com/incident-management/kpis. Incluye una discusión práctica sobre cuándo un rollback es preferible a un fix hacia adelante, en el contexto más amplio de responder incidentes con datos, no con intuición. En inglés.