Módulo 8: Project Ship Mercados Recommendations

Decidir: revertir en minutos, no en horas

Descripción

La lección anterior confirmó, con evidencia y no con intuición, que el guardrail de latencia se rompió en ramp-10: checkoutLatencyP95Ms en 910ms, techo 800ms, 173.3% del presupuesto consumido. Confirmar el problema no es lo mismo que resolverlo — alguien todavía tiene que decidir, en minutos y no en días, qué hacer con los 25,000 compradores que ya están expuestos. Esta lección toma esa decisión con rollbackDecision(), la ejecuta, y mide qué tan rápido respondió realmente el equipo con mttr().

Conexión con el módulo. Esta lección reutiliza rollbackDecision() y mttr() exactamente como quedaron en las lecciones 3 y 7 del módulo 5, sin ningún cambio. La entrada de rollbackDecision() en esta lección no es hipotética — es, literalmente, el haltEntry que confirmó la lección anterior: el mismo guardrail, el mismo valor de 910ms, el mismo techo de 800ms. Esa continuidad exacta —el dato que sale de una lección es el dato que entra en la siguiente, sin inventar nada nuevo— es el punto central de todo este módulo. Esta lección se detiene en el incidente resuelto; qué se aprende de él es el trabajo de la lección 5, que sigue después de esta.

Una analogía: el freno de emergencia, ya jalado

Hasta ahora, en este módulo, todo fue preparación: el flag cableado, la rampa diseñada, los instrumentos vigilados. Esta lección es distinta — es el momento en el que alguien, de verdad, jala el freno de emergencia del tren. No es un simulacro ni una discusión teórica sobre cuándo se debería jalar: es la decisión tomada, con el tren todavía en movimiento, sabiendo que cada segundo que pasa sin decidir es un segundo más de gente expuesta a un problema ya confirmado.

Un maquinista entrenado no jala el freno por instinto ni lo evita por miedo a las consecuencias de frenar — compara, en el momento, dos costos reales: cuánto cuesta frenar ahora (una sacudida, quizás una demora) contra cuánto cuesta seguir sin frenar mientras se investiga qué está pasando. rollbackDecision() es exactamente esa comparación, hecha con números en vez de con el pulso acelerado de la sala de incidentes.

Ejemplo trabajado: rollbackDecision() y mttr() sobre el incidente real

// M8 L04: decide revertir o arreglar hacia adelante sobre el guardrail confirmado roto
// de la leccion anterior, y mide el MTTR real de la respuesta. rollbackDecision() y
// mttr() son EXACTAMENTE las del modulo 5 (L3, L7), sin ningun cambio.

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 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: mttr sobre 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: mttr sobre 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)

La Parte 1 confirma la decisión con el mismo dato exacto que la lección 3 acaba de vigilar: p95Latency=910ms, un guardrail crítico roto. rollbackDecision() compara revertCost (2 minutos, gracias al kill switch que construyó el módulo 2) contra fixForwardCost (180 minutos, diagnosticar la causa técnica con la exposición activa) — y como revertir es muchísimo más barato, la decisión es rollback, sin que el +18.75% de checkoutConversion haya influido en absoluto en esa comparación.

La Parte 2 ejecuta esa decisión: en el mundo real, rollback significa recommendationsFlag.enabled = false — el mismo kill switch verificado en la lección 2 de este módulo, ahora activado de verdad, no en simulacro. El resultado se mide con precisión: los usuarios dejaron de estar expuestos a la latencia rota en apenas 3 minutos (timeToMitigate) — la gran mayoría del totalMTTR de 150 minutos (147 minutos) fue diagnóstico y verificación de la causa raíz, ya con el daño contenido, no exposición activa.

Por qué el kill switch de M2 es lo que hace posible este revertCost de 2 minutos

Vale la pena conectar, explícitamente, algo que las lecciones 2 y 4 de este módulo comparten: el revertCost: 2 que usa rollbackDecision() en esta lección no es un número arbitrario — es directamente el resultado de que el flag de recommendations, verificado en la lección 2, tenga un kill switch que apaga la exposición a cero con un solo cambio, sin necesidad de un nuevo deploy. Si recommendations no tuviera ese mecanismo —si "revertir" significara desplegar código nuevo, con todo el proceso de build, tests y deploy que eso implica—, el revertCost real sería mucho más alto, posiblemente comparable al fixForwardCost de 180 minutos, y la decisión de rollbackDecision() podría haber sido distinta.

Esto es, en una frase, por qué el módulo 2 —el flag— y el módulo 5 —la decisión de revertir— no son piezas independientes: el trabajo de construir un buen mecanismo de flag es lo que vuelve barata, después, la decisión de usarlo. Un lanzamiento sin flag no tiene esta opción disponible en absoluto.

Errores comunes

Calcular revertCost y fixForwardCost con estimaciones improvisadas en el momento del incidente. Qué pasa: alguien, en medio de la presión del HALT, inventa números aproximados para los dos costos —"revertir debe tomar unos minutos, arreglarlo debe tomar unas horas"— sin ninguna base real. Por qué pasa: bajo presión, cualquier número que "suene razonable" se siente suficiente para tomar una decisión rápida. Cómo detectarlo: si nadie puede explicar de dónde sale el 2 o el 180 de esta lección, los números son adivinanzas, no mediciones. Cómo corregirlo: como en esta lección, revertCost: 2 viene directamente del mecanismo ya verificado del kill switch (lección 2), y fixForwardCost: 180 viene de una estimación técnica real —diagnosticar la query lenta, parchearla, verificarla—, no de una intuición improvisada en el momento de la crisis.

Dejar que primary.win=true retrase la decisión, "por si acaso vale la pena reconsiderar". Qué pasa: alguien en la sala de incidentes, viendo el +18.75% de checkoutConversion, propone "esperar un poco más antes de revertir, a ver si el problema se resuelve solo" — dejando pasar minutos valiosos mientras la exposición continúa. Por qué pasa: un resultado positivo grande genera resistencia emocional a "deshacer" algo que parece estar funcionando. Cómo detectarlo: si la conversación sobre revertir se alarga más de lo que toma leer el código de rollbackDecision(), el resultado del primario probablemente está interfiriendo con una decisión que no debería depender de él. Cómo corregirlo: como confirma la Parte 1 de esta lección, la función nunca compara primary.win contra nada — la decisión se toma exclusivamente sobre el estado del guardrail crítico y el costo comparado de las dos rutas. Tenerla ya escrita, como código, es precisamente lo que evita esta demora en el momento real.

Reportar el totalMTTR de 150 minutos sin separar los dos tramos. Qué pasa: al comunicar el cierre del incidente, alguien dice "tardamos dos horas y media en resolverlo", sin aclarar que la exposición real de los usuarios terminó en el minuto 3. Por qué pasa: el número total es el más fácil de recordar y repetir, aunque no sea el que mejor describe el impacto real en los usuarios. Cómo detectarlo: si alguien concluye, después de tu reporte, que los compradores vieron la latencia rota durante dos horas y media, el reporte no comunicó bien la diferencia entre mitigar y resolver. Cómo corregirlo: como en la Parte 2 de esta lección, siempre reporta timeToMitigate (la exposición real, 3 minutos) por separado de timeToResolve (el diagnóstico completo con el daño ya contenido, 147 minutos adicionales).

Ejercicios

Ejercicio 1 — Cambia el costo del fix. Un ingeniero descubre, después de investigar un poco, que el problema en realidad es un valor de configuración mal puesto —no una query lenta—, y que corregirlo toma apenas 5 minutos, sin necesidad de desplegar código nuevo. Si este dato hubiera estado disponible antes de tomar la decisión de esta lección (revertCost se mantiene en 2), ¿cambiaría la decisión de rollbackDecision()?

Ver solución

No cambiaría: con fixForwardCost: 5 y revertCost: 2, la comparación revertCost <= fixForwardCost sigue siendo 2 <= 5, que es true — la decisión sigue siendo rollback. El punto de este ejercicio es notar que la brecha no tiene que ser enorme (2 contra 180) para que rollback sea la decisión correcta — incluso con un fixForwardCost mucho más bajo que el real, mientras siga siendo mayor o igual que el revertCost, revertir sigue siendo la opción más rápida disponible. La decisión cambiaría solo si fixForwardCost bajara a 2 o menos.

Ejercicio 2 — Calcula el MTTR con una línea de tiempo distinta. Supón que el equipo hubiera tardado 12 minutos en confirmar el guardrail (en vez de 3) antes de activar el kill switch —quizás por una alerta que no sonó de inmediato—, pero el resto de la investigación tomó el mismo tiempo. Con detected: '2026-03-04T14:12:00', mitigated: '2026-03-04T14:24:00' y resolved: '2026-03-04T16:42:00' (sin cambios), calcula timeToMitigate, timeToResolve y totalMTTR.

Ver solución

timeToMitigate = 12 minutos (14:12 a 14:24). timeToResolve = 138 minutos (14:24 a 16:42). totalMTTR = 150 minutos — el mismo total que en el caso real, porque detected y resolved no cambiaron. Lo que sí cambia es la distribución interna: 12 minutos de exposición activa en vez de 3, cuatro veces más — un recordatorio de que el totalMTTR puede quedarse igual mientras la parte que más le importa a los usuarios (timeToMitigate) empeora considerablemente. Comparar solo el total, sin ver el desglose, habría escondido esta diferencia real.

Ejercicio 3 — Defiende la decisión sin usar jerga técnica. En 2-3 frases, sin usar las palabras "rollback", "flag" ni "guardrail", explica a un familiar no técnico por qué Mercado apagó recommendations en vez de dejarlo prendido mientras arreglaban el problema.

Ver solución

Un ejemplo de respuesta: "Descubrimos que una nueva función de la app estaba haciendo que la página tardara más de lo aceptable en cargar para algunas personas. Como apagar esa función por completo tomaba solo un par de minutos y no afectaba nada más de la app, mientras que investigar y arreglar el problema específico hubiera tomado varias horas, decidimos apagarla primero para proteger a nuestros usuarios, e investigar con calma después, sin nadie esperando con la página lenta mientras tanto."

Resumen y siguiente paso

En esta lección tomaste la decisión que la lección anterior dejó pendiente: con un guardrail crítico roto (p95Latency=910ms) y un revertCost de apenas 2 minutos gracias al kill switch del módulo 2, rollbackDecision() decide rollback, sin que el resultado positivo del experimento (+18.75%) influyera en esa comparación. Medido con mttr(), la exposición real de los usuarios terminó en 3 minutos, dentro de un totalMTTR de 150 minutos donde el resto del tiempo fue diagnóstico con el daño ya contenido.

Antes de avanzar deberías poder: explicar por qué revertCost de esta lección es tan bajo específicamente gracias al trabajo de la lección 2; y calcular mttr() a mano dada una línea de tiempo con tres marcas horarias.

recommendations está, en este punto, resuelto y cerrado como incidente — pero "resuelto" no es lo mismo que "aprendido". La lección 5 convierte este mismo incidente en un postmortem completo: qué pasó, por qué, y qué cambia, sin culpar a ninguna persona del equipo.

Recursos

  • Charity Majors, "Deploys Are the WRONG Way to Change User Experience" — honeycomb.io/blog/deploys-wrong-way-change-user-experience. El argumento de fondo detrás de por qué revertir con un feature flag es casi siempre más rápido y más seguro que un rollback de infraestructura. En inglés.
  • Google SRE Book, Capítulo 14, "Managing Incidents" — sre.google/sre-book/managing-incidents. La disciplina completa de responder a un incidente con decisiones rápidas y medidas, en vez de improvisar bajo presión. En inglés.