Módulo 5: Rollback And Incident Response

Proyecto: responde de punta a punta al incidente de `recommendations`

Descripción

Las siete lecciones de este módulo construyeron, pieza por pieza, todo lo necesario para responder bien a un incidente real: la red de seguridad del kill switch y su costo (L2), el modelo que decide entre revertir y arreglar hacia adelante (L3), el runbook que ejecuta esa decisión sin improvisar (L4), el principio de mitigar antes de diagnosticar (L5), la clasificación de severidad (L6), y la medición del MTTR al cerrar (L7). Este proyecto te pone en la posición exacta del equipo de Mercado en el momento en que el módulo 4 confirmó el HALT: recommendations detenida en rollout 10%, 25,000 compradores expuestos, p95Latency en 910ms contra un techo de 800ms. Tu trabajo es responder de punta a punta, con las tres piezas ejecutables del módulo trabajando juntas sobre el mismo incidente.

Conexión con el módulo. Este proyecto no introduce ningún concepto nuevo — reutiliza rollbackDecision() tal como quedó en la lección 3, la estructura del runbook tal como quedó en la lección 4, y mttr() tal como quedó en la lección 7, y los combina en una sola respuesta continua. Es, literalmente, el ensayo del trabajo real que este módulo entero preparó. El proyecto se detiene exactamente donde el módulo se detiene: en el incidente resuelto y cerrado. Qué se aprende de él, con qué profundidad, y cómo se documenta para que no vuelva a pasar es el módulo 6, que sigue después de este.

Una analogía: el plan de vuelo de emergencia completo, no solo una pieza

Un piloto entrenado en emergencias no practica solo "apagar un motor" en el simulador, de forma aislada. Practica la secuencia completa: reconocer la falla, consultar el checklist correcto para ese tipo de falla exacto, ejecutar los pasos en orden, confirmar que la aeronave responde como se espera, y comunicar la situación a control de tráfico aéreo — de principio a fin, sin saltarse ningún tramo. Practicar solo una pieza aislada —solo el checklist, sin la decisión previa de qué tipo de emergencia es, o solo la comunicación, sin haber ejecutado la mitigación real— no prepara a nadie para el momento real, donde las piezas ocurren todas juntas, en secuencia, bajo presión.

Este proyecto es ese ensayo completo, aplicado al incidente de recommendations: no vas a decidir rollbackDecision() de forma aislada, sin conectarla con lo que sigue — vas a ver cómo esa decisión alimenta directamente el runbook, y cómo el runbook produce la línea de tiempo real que mttr() necesita para medir qué tan bien respondió el equipo.

Las tres partes, en un solo script

Este proyecto tiene tres partes —Parte 1: la decisión (rollbackDecision() sobre el incidente real), Parte 2: el runbook (los 5 pasos, ejecutados con la decisión ya tomada), y Parte 3: el MTTR (la línea de tiempo real, medida)— y corren en un solo script, porque la decisión de la Parte 1 alimenta directamente qué hace el paso mitigate del runbook en la Parte 2, y el runbook en sí es lo que produce los momentos mitigated que la Parte 3 necesita:

// PROYECTO M5: responde al incidente de recommendations en Mercado (guardrail de
// latencia roto en rollout 10%, M1/M3/M4). Decide rollback vs fix-forward con
// rollbackDecision(), ejecuta el runbook de 5 pasos, y calcula el MTTR de la linea de
// tiempo real del incidente con mttr().

function rollbackDecision({ guardrails, primary, revertCost, fixForwardCost }) {
  const brokenCritical = guardrails.filter((g) => g.critical && g.broken);
  if (brokenCritical.length === 0) return 'continue';
  return revertCost <= fixForwardCost ? 'rollback' : 'fix_forward';
}

function runRunbook(steps, state) {
  let current = { ...state };
  const log = [];
  for (const step of steps) {
    const result = step.action(current);
    current = { ...current, ...result.nextState };
    log.push({ id: step.id, name: step.name, outcome: result.outcome });
  }
  return { log, finalState: current };
}

function mttr(events) {
  const minutesBetween = (from, to) => Math.round((new Date(to) - new Date(from)) / 60000);
  return {
    timeToMitigate: minutesBetween(events.detected, events.mitigated),
    timeToResolve: minutesBetween(events.mitigated, events.resolved),
    totalMTTR: minutesBetween(events.detected, events.resolved),
  };
}

console.log('=== PARTE 1: rollbackDecision sobre recommendations (rollout 10%) ===\n');
const mercadoGuardrails = [
  { name: 'p95Latency', critical: true, broken: true, value: 910, ceiling: 800 },
  { name: 'checkoutErrorRate', critical: true, broken: false, value: 0.006, ceiling: 0.02 },
];
const mercadoPrimary = { name: 'checkoutConversion', win: true, lift: 0.1875 };
const decision = rollbackDecision({
  guardrails: mercadoGuardrails,
  primary: mercadoPrimary,
  revertCost: 2,
  fixForwardCost: 180,
});
console.log('primary.win=' + mercadoPrimary.win + ' (+' + (mercadoPrimary.lift * 100) + '% checkoutConversion) -- NO anula el guardrail roto');
console.log('guardrail roto: p95Latency=' + mercadoGuardrails[0].value + 'ms (techo ' + mercadoGuardrails[0].ceiling + 'ms)');
console.log('revertCost=2min (kill switch) vs fixForwardCost=180min (diagnosticar + parchear + verificar)');
console.log('-> decision: ' + decision);

console.log('\n=== PARTE 2: ejecuta el runbook de 5 pasos ===\n');
const runbook = [
  { id: 1, name: 'acknowledge', action: (s) => ({
    outcome: 'on-call reconoce la alerta del dashboard de guardrails (M4)',
    nextState: { acknowledged: true },
  }) },
  { id: 2, name: 'confirm guardrail', action: (s) => ({
    outcome: s.p95Latency > s.ceiling ? 'guardrail CONFIRMADO roto (' + s.p95Latency + 'ms > ' + s.ceiling + 'ms)' : 'no era real',
    nextState: { guardrailConfirmed: s.p95Latency > s.ceiling },
  }) },
  { id: 3, name: 'mitigate', action: (s) => ({
    outcome: decision === 'rollback' ? 'recommendationsFlag.enabled = false (kill switch, M2 L5)' : 'aplicar hotfix acotado (fix-forward)',
    nextState: { mitigated: true },
  }) },
  { id: 4, name: 'verify', action: (s) => ({
    outcome: 'p95Latency vuelve a ~650ms para todos los usuarios',
    nextState: { verified: true },
  }) },
  { id: 5, name: 'communicate', action: (s) => ({
    outcome: 'status update enviado: mitigado, SEV2, investigando causa raiz',
    nextState: { communicated: true },
  }) },
];
const { log } = runRunbook(runbook, { p95Latency: 910, ceiling: 800 });
log.forEach((l) => console.log('paso ' + l.id + ' (' + l.name + '): ' + l.outcome));

console.log('\n=== PARTE 3: MTTR de la linea de tiempo real del incidente ===\n');
const timeline = {
  detected: '2026-03-04T14:12:00',
  mitigated: '2026-03-04T14:15:00',
  resolved: '2026-03-04T16:42:00',
};
const result = mttr(timeline);
console.log('detected   ' + timeline.detected);
console.log('mitigated  ' + timeline.mitigated + '  (timeToMitigate=' + result.timeToMitigate + 'min)');
console.log('resolved   ' + timeline.resolved + '  (timeToResolve=' + result.timeToResolve + 'min)');
console.log('totalMTTR=' + result.totalMTTR + 'min (' + (result.totalMTTR / 60).toFixed(1) + 'h)');

Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:

=== PARTE 1: rollbackDecision sobre recommendations (rollout 10%) ===

primary.win=true (+18.75% checkoutConversion) -- NO anula el guardrail roto
guardrail roto: p95Latency=910ms (techo 800ms)
revertCost=2min (kill switch) vs fixForwardCost=180min (diagnosticar + parchear + verificar)
-> decision: rollback

=== PARTE 2: ejecuta el runbook de 5 pasos ===

paso 1 (acknowledge): on-call reconoce la alerta del dashboard de guardrails (M4)
paso 2 (confirm guardrail): guardrail CONFIRMADO roto (910ms > 800ms)
paso 3 (mitigate): recommendationsFlag.enabled = false (kill switch, M2 L5)
paso 4 (verify): p95Latency vuelve a ~650ms para todos los usuarios
paso 5 (communicate): status update enviado: mitigado, SEV2, investigando causa raiz

=== PARTE 3: MTTR de la linea de tiempo real del incidente ===

detected   2026-03-04T14:12:00
mitigated  2026-03-04T14:15:00  (timeToMitigate=3min)
resolved   2026-03-04T16:42:00  (timeToResolve=147min)
totalMTTR=150min (2.5h)

Fíjate en el hilo que conecta las tres partes. La Parte 1 decide rollback porque hay un guardrail crítico roto y el kill switch cuesta muchísimo menos que un fix hacia adelante — y esa palabra, rollback, es la que el paso mitigate de la Parte 2 lee directamente (decision === 'rollback') para saber qué acción ejecutar: apagar el flag, no aplicar un parche. La Parte 3 mide el resultado real de esa cadena de decisiones: timeToMitigate de apenas 3 minutos —la exposición real de los usuarios terminó casi de inmediato— y un totalMTTR de 150 minutos, donde la gran mayoría del tiempo (147 minutos) fue diagnóstico y verificación con el daño ya contenido, no exposición activa.

El resultado, en limpio

PiezaEntrada claveSalida
rollbackDecision()guardrail crítico roto (p95Latency), revertCost=2min, fixForwardCost=180minrollback
runbook (5 pasos)p95Latency=910ms (techo 800ms), decisión = rollbackkill switch activado, verificado, comunicado
mttr()detected → mitigated → resolvedtimeToMitigate=3min, totalMTTR=150min (2.5h)

El valor concreto de este proyecto no es un solo número — es la cadena completa: el guardrail roto disparó una decisión con criterio explícito, esa decisión se ejecutó siguiendo pasos pre-escritos sin que nadie improvisara, y el resultado medible de esa cadena fue que los usuarios de Mercado quedaron protegidos en 3 minutos, no en 150.

Errores comunes

Correr rollbackDecision() sin conectarla con el runbook, tratándolas como dos ejercicios separados. Qué pasa: alguien calcula la decisión (rollback), pero al ejecutar el runbook lo hace de forma genérica, sin que el paso mitigate realmente lea qué decisión se tomó — como si el runbook fuera un checklist fijo que no depende de nada anterior. Por qué pasa: cada pieza se construyó en una lección separada, y es natural pensar en ellas como módulos independientes en vez de una cadena conectada. Cómo detectarlo: si el paso mitigate del runbook haría exactamente lo mismo sin importar qué devolviera rollbackDecision(), la conexión no existe de verdad. Cómo corregirlo: como en el código de este proyecto, el paso mitigate usa decision === 'rollback' explícitamente para decidir su acción — la decisión de la Parte 1 no es un ejercicio aparte, es un insumo real de la Parte 2.

Reportar el totalMTTR de 150 minutos sin aclarar que el timeToMitigate fue de solo 3. Qué pasa: al comunicar el resultado de este proyecto hacia arriba, alguien dice "el incidente tardó dos horas y media en resolverse", dejando la impresión de que los usuarios estuvieron expuestos todo ese tiempo. Por qué pasa: el número más grande y redondo (150min, 2.5h) es el que más fácilmente se recuerda y se repite, aunque no sea el que más le importa a la gente afectada. Cómo detectarlo: si alguien pregunta "¿entonces los usuarios vieron la latencia rota durante dos horas y media?" después de escuchar tu reporte, el reporte no comunicó bien la diferencia entre mitigar y resolver. Cómo corregirlo: como enfatizó la lección 7, siempre reporta los dos tramos por separado — la exposición real terminó en el minuto 3; el resto del tiempo fue diagnóstico con el daño ya contenido.

Tratar el incidente "resuelto" de la Parte 3 como el final completo de la historia. Qué pasa: el equipo cierra el incidente con el mensaje de comunicación del paso 5, el MTTR calculado, y da por terminado todo el trabajo — sin plantear qué se aprendió de esto, ni si hace falta cambiar algo en el proceso para que no se repita. Por qué pasa: mttr() da un cierre numérico satisfactorio, y ese cierre puede sentirse, erróneamente, como el final completo del ciclo. Cómo detectarlo: si nadie tiene asignada la tarea de escribir qué pasó, por qué, y qué cambiar, después de que el incidente está técnicamente resuelto, falta un paso. Cómo corregirlo: como señaló la lección 1 de este módulo, "resuelto" no es "aprendido" — el módulo 6, que sigue después de este proyecto, construye exactamente esa pieza: el postmortem blameless que convierte este incidente en aprendizaje real para el equipo.

Ejercicios de transferencia

A diferencia de los ejercicios de las lecciones anteriores, este proyecto te pide aplicar el método completo a un caso que este módulo nunca vio — la prueba real de si aprendiste a responder a un incidente o solo memorizaste el resultado de recommendations.

Ejercicio 1 — Cambia el costo del fix y observa el efecto en cadena. Supón que, para este mismo incidente de recommendations, un ingeniero descubre que el problema es, en realidad, un valor de configuración mal puesto —no una query lenta— y que corregirlo toma apenas 1 minuto, sin necesidad de desplegar código nuevo (fixForwardCost: 1). Ejecuta mentalmente rollbackDecision() con este cambio (revertCost se mantiene en 2). ¿Cambia la decisión? Si el paso mitigate del runbook usara esta nueva decisión, ¿qué texto de outcome mostraría en vez del que viste en la Parte 2?

Ver solución

Con fixForwardCost: 1 y revertCost: 2, la comparación revertCost <= fixForwardCost se vuelve 2 <= 1, que es false — la decisión cambia a fix_forward. El paso mitigate del runbook, que lee decision === 'rollback' para decidir su acción, ahora tomaría la rama contraria: en vez de 'recommendationsFlag.enabled = false (kill switch, M2 L5)', mostraría 'aplicar hotfix acotado (fix-forward)' — exactamente el texto que ya estaba escrito en el código como alternativa, pero que nunca se ejecutó en la Parte 2 original porque la decisión real fue rollback. Este ejercicio confirma que la conexión entre las partes es real: cambiar la Parte 1 cambia, automáticamente, el comportamiento de la Parte 2, sin que haga falta tocar el código del runbook en absoluto.

Ejercicio 2 — Aplica el método completo a un caso nuevo. El equipo de logística de Mercado (del ejercicio 2 del proyecto del módulo 1, y del ejercicio 1 del módulo 3) tiene su algoritmo de tiempos de entrega estimados corriendo en rollout 10%, y su guardrail de errorRate se rompe: 4.6%, contra un techo de 4%. El equipo mide revertCost: 3 minutos (tienen su propio kill switch, siguiendo el mismo patrón del módulo 2) y fixForwardCost: 25 minutos (el bug es un caso borde ya identificado, con un fix acotado y ya escrito, pendiente solo de desplegar). Aplica rollbackDecision() a este caso. Después, calcula el MTTR sabiendo que el guardrail se detectó a las 11:00:00, se mitigó a las 11:03:00, y el equipo confirmó la resolución completa a las 11:35:00.

Ver solución

Con guardrails: [{ critical: true, broken: true, ... }] (errorRate roto) y revertCost: 3 <= fixForwardCost: 25, la decisión es rollback — igual que en recommendations, aunque por un margen mucho más corto entre las dos opciones. Con la línea de tiempo dada, mttr() calcularía: timeToMitigate = 3 minutos (11:00 a 11:03), timeToResolve = 32 minutos (11:03 a 11:35), y totalMTTR = 35 minutos (11:00 a 11:35). Nota lo distinto que es este caso del de recommendations: aunque el veredicto final (rollback) es el mismo, la diferencia entre revertCost y fixForwardCost es mucho más chica (3 contra 25, no 2 contra 180) — si el fix acotado del equipo de logística hubiera tomado, en cambio, 2 minutos en vez de 25, la decisión habría cambiado a fix_forward. Aplicar el mismo modelo a un caso nuevo no significa que el resultado siempre sea igual — significa usar el mismo criterio explícito para llegar a la respuesta que corresponde a estos números.

Ejercicio 3 — Escribe el cierre completo del incidente. Combinando los resultados de las tres partes de este proyecto —la decisión, el runbook ejecutado, y el MTTR—, escribe el mensaje de cierre (100-150 palabras) que el equipo de recommendations enviaría al resto de la organización de Mercado, incluyendo: qué se decidió y por qué, qué severidad tuvo (SEV2, de la lección 6), y los dos tramos del MTTR por separado.

Ver solución

Un mensaje posible: "Cierre de incidente SEV2 — recommendations, rollout 10%. A las 14:12 el dashboard de guardrails confirmó que p95Latency volvió a romper el techo de 800ms (910ms medidos), afectando a 25,000 compradores. Con un guardrail crítico roto y el kill switch disponible en apenas 2 minutos —frente a un fix hacia adelante estimado en 3 horas—, decidimos revertir de inmediato en vez de investigar con la exposición activa. La mitigación se completó a las 14:15 (timeToMitigate: 3 minutos); ningún usuario quedó expuesto después de ese momento. La causa raíz —una query sin índice bajo tráfico real— quedó diagnosticada, corregida y verificada a las 16:42 (timeToResolve: 147 minutos adicionales, sin impacto a usuarios durante ese tramo). MTTR total: 150 minutos. El rollout permanece frenado en 10% hasta la próxima decisión de reintento." El mensaje separa con precisión los dos tramos del MTTR, nombra la severidad, y explica el criterio de la decisión sin dejar espacio para que alguien concluya, incorrectamente, que los usuarios estuvieron expuestos los 150 minutos completos.

Resumen y siguiente paso

En este proyecto respondiste de punta a punta al incidente de recommendations: rollbackDecision() decidió rollback frente al guardrail de latencia roto, sin que el resultado positivo del experimento influyera en esa decisión; el runbook de 5 pasos ejecutó esa decisión en orden, sin improvisar, confirmando el guardrail antes de mitigar y verificando el efecto después; y mttr() midió el resultado real: 3 minutos de exposición efectiva, dentro de un totalMTTR de 150 minutos donde el resto del tiempo fue diagnóstico completo, ya con el daño contenido.

Con esto cierras el módulo 5: tienes la mecánica completa de la respuesta a un incidente —la red de seguridad, la decisión entre revertir y arreglar hacia adelante, el runbook, el orden correcto de mitigar antes de diagnosticar, la severidad, y la medición del MTTR— aplicada de principio a fin sobre el incidente real de recommendations.

Hacia dónde sigues. El módulo 6 toma este mismo incidente, ya resuelto y cerrado, y construye lo que este módulo dejó pendiente explícitamente: el postmortem blameless —la investigación completa de la causa raíz, con su timeline y sus factores contribuyentes, sin culpar a ninguna persona— y el ciclo ship → measure → learn, que cierra iterando sobre lo aprendido. El módulo 7 aplica el mismo marco de rollback y mitigación cuidadosa a la capa moderna de enviar modelos de IA con seguridad. El módulo 8, el capstone de toda la guía, te va a pedir ejecutar las ocho piezas completas —flag, rollout, monitoreo, esta respuesta al incidente, y el postmortem— sobre este mismo lanzamiento de recommendations, de principio a fin.

Recursos

  • Google SRE Book, Capítulo 14, "Managing Incidents" — sre.google/sre-book/managing-incidents. La referencia formal completa de la disciplina que este proyecto ejecutó de punta a punta: detección, decisión, mitigación estructurada, y cierre medido — no como pasos aislados, sino como una cadena continua. En inglés.
  • PagerDuty Incident Response Documentation — response.pagerduty.com. Cierra el módulo donde lo abrió la lección 1: la guía práctica completa de la industria sobre responder a un incidente de principio a fin, con la misma estructura —reconocer, mitigar, verificar, comunicar, medir— que este proyecto ejecutó en Node. En inglés.