CodeGym /Cursos /ChatGPT Apps /Resiliencia del sistema: timeouts, circuit breakers, bulk...

Resiliencia del sistema: timeouts, circuit breakers, bulkheads, protección frente a tormentas de webhooks

ChatGPT Apps
Nivel 16 , Lección 2
Disponible

1. Por qué pensar en la «resiliencia» en una ChatGPT App

En una aplicación web normal el usuario al menos ve la URL, el spinner del navegador, puede recargar la página. En ChatGPT el usuario ve una sola pantalla: el chat y tu App. Si algo va lento, no distingue quién tiene la culpa — OpenAI, tu Gateway, la pasarela de pago o el microservicio de analítica del vecino. Para él todo es «ChatGPT + tu App».

Cuando un tool-call se queda colgado 3060 segundos, el modelo espera y espera… y, en el mejor de los casos, se disculpa por el retraso. En el peor, se inventa la respuesta en lugar de usar los datos de tu backend. Por eso la resiliencia no es solo cosa de SRE y del uptime, también trata de la calidad de la respuesta, el tono del modelo y las métricas en el Store.

En el ecosistema de una ChatGPT App tenemos varios circuitos independientes:

  • ChatGPT ↔ MCP Gateway.
  • Gateway ↔ tus servicios backend/REST (Gift REST API, Commerce REST API, Analytics Service, etc.).
  • Tus servicios ↔ APIs externas (LLM, pagos, catálogos).
  • Webhooks entrantes (ACP, Stripe, cualquier integración) ↔ tus manejadores.

El problema es que un fallo en un punto puede provocar un efecto dominó: el Gateway espera obedientemente a un servicio colgado, los workers se saturan, se agotan las conexiones, los clientes empiezan a reintentar y, al cabo de un par de minutos, tienes el clásico escenario en el que todo arde y se hunde a la vez. Precisamente de esto nos protegen los cuatro patrones de los que hablamos hoy:

  • Timeouts — nunca esperamos eternamente.
  • Circuit breaker — no nos damos contra una puerta cerrada.
  • Bulkheads — construimos «compartimentos» y evitamos que se hunda todo el barco.
  • Protección frente a tormentas de webhooks — asumimos que los webhooks llegan con duplicados, picos y reintentos, y nos preparamos.

2. Timeouts: no esperamos eternamente

Qué es un timeout y por qué sin él todo va mal

Un timeout es el tiempo máximo que tu código está dispuesto a esperar la respuesta de una dependencia: base de datos, servidor MCP, HTTP API externo, modelo. Si la respuesta no llega en el tiempo fijado, consideramos la llamada fallida, liberamos recursos y devolvemos un error comprensible o un fallback.

Sin timeouts las solicitudes pueden:

  • quedarse esperando indefinidamente,
  • ocupar conexiones y el pool de hilos/recursos,
  • bloquear peticiones posteriores,
  • provocar fallos en cascada.

El patrón es sencillo: «mejor un fallo predecible a los 35 segundos que un silencio inexplicable durante 5 minutos».

Es importante recordar que hay timeouts en varios niveles:

  • a nivel de proxy/balanceador (Cloudflare, Nginx),
  • a nivel de MCP Gateway (clientes HTTP hacia microservicios),
  • en los propios servicios (llamadas a la BD, APIs externas, LLM).

Para ChatGPT en general, es razonable aspirar a un tiempo total de tool-call en el rango de 510 segundos para operaciones normales y un máximo de 2030 segundos para las especialmente pesadas. Más que eso suele ser casi garantía de mala UX.

fetchWithTimeout sencillo en TypeScript

Empecemos por la práctica. En el MCP Gateway de GiftGenius tenemos un cliente HTTP auxiliar que llama al recomendador de regalos, al servicio de commerce y a analítica. Vamos a envolver el fetch estándar en una función con timeout:

// src/gateway/httpClient.ts
export async function fetchWithTimeout(
  url: string,
  opts: RequestInit & { timeoutMs?: number } = {}
) {
  const { timeoutMs = 5000, ...rest } = opts;
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

  try {
    return await fetch(url, { ...rest, signal: controller.signal });
  } finally {
    clearTimeout(timeoutId);
  }
}

Ahora, en el código del Gateway nunca hacemos un fetch «a pelo», solo a través de este helper:

// src/gateway/giftClient.ts
import { fetchWithTimeout } from "./httpClient";

export async function callGiftService(path: string) {
  const res = await fetchWithTimeout(
    process.env.GIFT_SERVICE_URL + path,
    { timeoutMs: 4000 }
  );

  if (!res.ok) {
    throw new Error(`gift_service_${res.status}`);
  }
  return res.json();
}

Este enfoque garantiza que, incluso si el servicio de regalos se cuelga, a los 4 segundos cortaremos la conexión y podremos devolver un error MCP a ChatGPT, en lugar de mantener la conexión agotándose.

Dónde exactamente poner los timeouts en GiftGenius

En nuestro ejemplo de GiftGenius:

  • A nivel de Gateway: timeouts en las llamadas a Gift REST API, Commerce REST API, Analytics Service / REST API.
  • Dentro de esos servicios: timeouts en las llamadas a la BD, ACP/pasarelas de pago y APIs de recomendación externas.
  • En la entrada del Gateway: un timeout general de la solicitud desde ChatGPT para que el tool-call no se convierta en un «spinner eterno».

Es importante que el tiempo de espera en el nivel superior sea un poco mayor que en los internos. Por ejemplo, si el Gateway espera al backend 5 segundos y el backend espera a la BD 3 segundos, entonces tenemos margen para procesar y serializar el resultado.

Cómo explicar los timeouts al modelo de ChatGPT

Para ChatGPT es importante devolver errores semánticos, y no tirar la conexión en silencio. En vez de un 500 abstracto, mejor devolver un error MCP estructurado que el modelo pueda verbalizar al usuario: «El servicio de selección de regalos está sobrecargado, inténtalo de nuevo un poco más tarde», etc.

Esto significa que, en el Gateway, cuando hay timeout hay que:

  1. Atrapar AbortError o nuestro timeout_….
  2. Formar una respuesta MCP con un código significativo y una descripción breve.
  3. Dar al modelo la posibilidad de decidir cómo explicarlo a la persona.

Los timeouts resuelven el problema de las solicitudes colgadas, pero si una dependencia empieza a fallar masivamente, no nos protegen de la avalancha de intentos idénticos fallidos. Aquí necesitamos el siguiente nivel de protección: el circuit breaker.

3. Circuit breaker: «automático» frente a servicios moribundos

Intuición: por qué un timeout no basta

Ya hemos aprendido a limitar el tiempo de espera de llamadas individuales mediante timeouts. El timeout protege una llamada concreta. Pero si la dependencia está «muerta de verdad» (por ejemplo, el servicio de commerce cae por OOM (Out Of Memory) en cada petición), seguiremos llamándolo, esperando 35 segundos cada vez, capturando el error, cargando la red y la CPU, y volviendo a esperar.

El circuit breaker añade memoria: rastrea errores y timeouts y, cuando son demasiados, deja de enviar peticiones a ese servicio. En su lugar devuelve un rechazo rápido o un fallback. Tras un tiempo, prueba de nuevo con cautela en modo half-open.

Estados clásicos del automático:

  • Closed — todo normal, las peticiones pasan.
  • Open — el servicio se considera «muerto», no se envían peticiones, error inmediato.
  • Half-open — probamos un número limitado de peticiones; si tienen éxito, volvemos a closed; si fallan de nuevo, regresamos a open.

Esquema simple de circuit breaker

Pequeño diagrama:

stateDiagram-v2
    [*] --> Closed
    Closed --> Open: demasiados errores
    Open --> HalfOpen: se agotó el cooldown
    HalfOpen --> Closed: varios éxitos seguidos
    HalfOpen --> Open: errores de nuevo
    Open --> Open: rechazo rápido

Miniimplementación de circuit breaker en TypeScript

En producción suelen usarse bibliotecas ya hechas (para Node.js están, por ejemplo, opossum o soluciones ligeras self-made), pero para entender la mecánica basta con una clase compacta.

Ejemplo de breaker muy simplificado alrededor de la llamada al módulo de commerce:

// src/gateway/circuitBreaker.ts
type State = "closed" | "open" | "half-open";

export class CircuitBreaker {
    private state: State = "closed";
    private failureCount = 0;
    private nextAttemptAt = 0;

    constructor(
        private readonly failureThreshold = 5,
        private readonly cooldownMs = 30_000
    ) {}

    async call<T>(fn: () => Promise<T>): Promise<T> {
        const now = Date.now();

        if (this.state === "open") {
            if (now < this.nextAttemptAt) {
                throw new Error("circuit_open");
            }
            this.state = "half-open";
        }

        try {
            const result = await fn();
            this.onSuccess();
            return result;
        } catch (err) {
            this.onFailure();
            throw err;
        }
    }

    private onSuccess() {
        this.failureCount = 0;
        this.state = "closed";
    }

    private onFailure() {
        this.failureCount++;
        if (this.failureCount >= this.failureThreshold) {
            this.state = "open";
            this.nextAttemptAt = Date.now() + this.cooldownMs;
        }
    }
}

Y uso en el cliente del servicio de commerce:

// src/gateway/commerceClient.ts
const commerceBreaker = new CircuitBreaker(3, 20_000);

export async function callCommerce(path: string) {
    return commerceBreaker.call(async () => {
        const res = await fetchWithTimeout(
            process.env.COMMERCE_URL + path,
            { timeoutMs: 3000 }
        );
        if (!res.ok) throw new Error(`commerce_${res.status}`);
        return res.json();
    });
}

Aquí, cuando commerce empieza a responder con errores de forma masiva o no llega al timeout, tras varios fallos el breaker pasa a open. En ese estado, durante cooldownMs ni siquiera intentamos llamar al servicio y devolvemos al instante el error circuit_open.

Qué debería ver ChatGPT cuando el breaker ha «cortado» el servicio

Desde la perspectiva de ChatGPT, mejor si:

  • Respondes con rapidez con un error MCP del tipo «commerce_unavailable» o «gift_service_overloaded».
  • Añades una descripción comprensible: «El servicio de pagos está temporalmente indisponible, probemos más tarde».
  • No ocultas el error tras reintentos interminables.

Este es justo el caso en el que un «rechazo rápido y honesto» es mejor que una larga espera. Especialmente en el checkout: el usuario aceptará mejor un mensaje claro que mirar 40 segundos un spinner para acabar con un «algo ha ido mal».

Los timeouts y el breaker nos protegen de dependencias «malas» o caídas, pero no resuelven el problema de cuando un tipo de carga se come todos los recursos y empieza a asfixiar el resto del sistema. Para eso necesitamos otra capa: los bulkheads.

4. Bulkheads: aislamiento de «compartimentos» para que uno no hunda todo el barco

Analogía con un barco

El patrón bulkhead recibe su nombre de los mamparos de un barco: si en un compartimento hay una vía de agua, esta no se propaga por todo el barco. En arquitectura significa: dividir los recursos entre distintos frentes de trabajo para que un servicio saturado no se coma todo — CPU, conexiones, pools — y no tumbe las rutas críticas.

En microservicios esto suele hacerse con:

  • pools de conexiones HTTP separados,
  • pools de hilos/workers,
  • colas/tópicos separados,
  • incluso clusters de BD separados para operaciones críticas.

La idea es que, si el servicio de recomendaciones de regalos empieza a ir lento y se atasca, solo agotará sus recursos, pero no romperá el checkout ni la autorización.

Bulkheads en el mundo de Node.js y MCP Gateway

En Node.js no tenemos hilos en el sentido clásico (hay event loop y workers), pero podemos limitar la cantidad de tareas en paralelo para cada frente.

Ejemplo: en el Gateway hay tres dependencias externas:

  • Servicio de Gift (selección de regalos, llamadas LLM pesadas).
  • Servicio de Commerce (checkout, ACP).
  • Servicio de Analytics (registro de eventos).

Podemos introducir límites sencillos para las solicitudes simultáneas a cada uno.

Por ejemplo, un pequeño «semáforo» para limitar la paralelización:

// src/gateway/bulkhead.ts
export class Bulkhead {
    private active = 0;
    private queue: (() => void)[] = [];

    constructor(private readonly maxConcurrent: number) {}

    async run<T>(fn: () => Promise<T>): Promise<T> {
        if (this.active >= this.maxConcurrent) {
            await new Promise<void>((resolve) => this.queue.push(resolve));
        }
        this.active++;

        try {
            return await fn();
        } finally {
            this.active--;
            const next = this.queue.shift();
            if (next) next();
        }
    }
}

Y su uso para los servicios:

// src/gateway/clients.ts
import { Bulkhead } from "./bulkhead";

const giftBulkhead = new Bulkhead(10);      // hasta 10 en paralelo
const commerceBulkhead = new Bulkhead(3);   // el checkout está muy limitado
const analyticsBulkhead = new Bulkhead(50); // permite muchos

export async function callGiftWithBulkhead(fn: () => Promise<any>) {
    return giftBulkhead.run(fn);
}

export async function callCommerceWithBulkhead(fn: () => Promise<any>) {
    return commerceBulkhead.run(fn);
}

Así, incluso si GPT decide pedir en masa «hazme 30 selecciones de regalos complejas», se ejecutarán como máximo 10 a la vez, y el checkout podrá seguir funcionando con su propio límite independiente.

GiftGenius: qué compartimentos queremos

En GiftGenius tiene sentido crear compartimentos separados para:

  • Selección de regalos (pesado en LLM, menos crítico, puede ralentizarse).
  • Checkout/ACP (súper crítico, hay que protegerlo al máximo).
  • Analítica/logs (importante, pero puede tolerar cierto retraso).

En una arquitectura más avanzada también los desplegarías como clusters distintos con recursos separados, pero en el contexto de esta lección lo importante es la idea: no dejar que funciones secundarias «se coman» todo el oxígeno.

Estos tres patrones — timeouts, circuit breaker y bulkheads — tratan de cómo sales hacia fuera, a tus dependencias. Pero hay otra clase de amenazas para la resiliencia: los flujos entrantes de eventos que pueden tumbarte incluso con llamadas salientes perfectamente configuradas. El ejemplo clásico son las tormentas de webhooks.

5. Tormentas de webhooks: cuando el mundo te envía eventos más rápido de lo que puedes procesar

Cómo se comportan los webhooks en la realidad

La cuarta fuente de problemas de resiliencia son los eventos entrantes: webhooks de ACP, Stripe y otros sistemas. Son los que pueden montar una auténtica «tormenta», incluso si ya tienes timeouts, circuit breakers y bulkheads configurados.

Los webhooks no son una petición HTTP «bajo demanda», sino eventos push de sistemas externos (Stripe, ACP, tiendas externas, etc.). Tienen varias propiedades incómodas:

  • Entrega al menos una vez (at-least-once) — por tanto, los duplicados son inevitables.
  • No se garantiza el orden de entrega.
  • Ante errores suelen reintentar: primero al segundo, luego a los 10, luego al minuto… hasta que respondas 2xx.
  • En picos (por ejemplo, en rebajas) llegan por tandas, creando una «tormenta».

Si tu manejador no es idempotente y tarda demasiado, se convierte en un cuello de botella, la cola se atasca y los reintentos solo empeoran la tormenta. Puedes acabar tumbando la base de datos, la cola, los pools de workers y, en cadena, el resto del sistema.

Principios básicos de protección frente a tormentas

Hay varias ideas que aumentan mucho las probabilidades de sobrevivir a una tormenta:

Primero, queue-first, process-later. Idealmente, el webhook entrante no debe ejecutar trabajo pesado de forma síncrona. En su lugar, valida firma/formato lo más rápido posible, coloca la tarea en una cola y responde 200 OK. El procesamiento va de forma asíncrona en un worker. Si necesitas «confirmación rápida» para ChatGPT, puedes tener un circuito de notificaciones aparte.

Segundo, idempotencia del manejador. Un webhook repetido para la misma operación no debe «crear el pedido otra vez» ni «cobrar dos veces». Normalmente se resuelve guardando un idempotency key o eventId y comprobando si ya hemos procesado ese evento.

Tercero, rate limiting y circuit breaker en el receptor. Aunque el emisor esté en tormenta, puedes:

  • limitar el RPS por IP/suscripción/endpoint,
  • devolver temporalmente 429 o 503 para desacelerar los reintentos,
  • usar breaker para no verter el flujo en un downstream roto (por ejemplo, la BD de pedidos).

Ejemplo de manejador de webhook en Next.js para GiftGenius

Imaginemos que tenemos un ACP/pasarela que envía un webhook con el estado del pedido a POST /api/commerce/webhook. Queremos:

  • aceptar el evento rápido y meterlo en una cola,
  • no procesarlo de forma síncrona,
  • no rompernos con los duplicados.

Ejemplo simplificado (sin verificación de firma ni cola real — eso estará en los módulos de seguridad y colas):

// app/api/commerce/webhook/route.ts
import { NextRequest, NextResponse } from "next/server";

// Aquí podríamos tener Redis/cola; de momento simulamos con un array
const inMemoryQueue: any[] = [];
const processedEvents = new Set<string>(); // idempotencia (para la demo)

export async function POST(req: NextRequest) {
    const event = await req.json();

    const eventId = event.id as string;
    if (processedEvents.has(eventId)) {
        return NextResponse.json({ ok: true, duplicate: true });
    }

    // En la realidad aquí habrá verificación de firma y de esquema

    inMemoryQueue.push(event); // metemos en la cola para procesamiento en background
    // Un worker en background lo procesará más tarde y marcará el ID como procesado
    return NextResponse.json({ ok: true });
}

De momento es una pseudoimplementación, pero hay dos puntos clave:

  1. La parte síncrona es lo más ligera posible.
  2. Establecemos idempotencia alrededor de event.id.

En la vida real vas a:

  • usar una cola externa (SQS, RabbitMQ, Kafka),
  • guardar los eventos procesados en la BD,
  • verificar la firma del webhook y la versión del payload,
  • quizá aplicar un Bulkhead/Breaker aparte alrededor del manejador.

Cómo se ve esto en el contexto de GiftGenius

Para GiftGenius, integrado con ACP/Stripe vía webhooks, la protección frente a tormentas es especialmente importante en temporadas pico (Año Nuevo, Black Friday). Hay muchos eventos:

  • creación de intents,
  • confirmaciones de pago,
  • cancelaciones,
  • devoluciones.

Si tu manejador empieza a «alargarse» (por ejemplo, por llamadas a APIs externas), te arriesgas a que:

  • ACP empiece a reintentar,
  • los eventos lleguen por tandas,
  • la BD de pedidos y el pool de workers se saturen.

El patrón «queue first» + idempotencia + rate limiting en la entrada sirve precisamente de seguro frente a estos escenarios.

6. Cómo funcionan juntos estos patrones

Ahora unimos todos estos patrones en un único escenario y vemos cómo funcionan en el flujo real «Elige un regalo y tramita el pedido ahora».

Consideremos la cadena «ChatGPT → Gateway → Gift Service → Commerce → webhooks» con el siguiente escenario:

El usuario en el chat dice: «Elige un regalo y tramita el pedido ahora mismo».

  1. El modelo decide llamar a tu tool suggest_and_checkout.
  2. El Gateway llama al servicio de regalos mediante fetchWithTimeout y el bulkhead del servicio de regalos.
  3. Si el servicio de regalos se cuelga, salta el timeout; el breaker alrededor de él, tras cierto número de errores, pasará a open, y las siguientes solicitudes recibirán enseguida el error MCP «gift_service_unavailable».
  4. Si el servicio de regalos responde, el Gateway llama al servicio de commerce (otra vez con timeout y su bulkhead separado).
  5. Cualquier problema con commerce activa un circuit breaker aparte, configurado más estricto que el de gift (porque el checkout es crítico).
  6. Un pedido exitoso provoca un webhook de ACP a tu /api/commerce/webhook, que mete el evento en una cola y responde rápido; los workers en background procesan el pago, y los webhooks repetidos con el mismo eventId se ignoran como duplicados.

Como resultado:

  • Un servicio de selección colgado no tumba el checkout.
  • Un commerce colgado no convierte todos los tool-calls en un spinner de un minuto — ChatGPT recibe rápido un error con sentido.
  • Las tormentas de webhooks no rompen tu circuito HTTP principal.
  • Controlas los puntos de degradación: mejor desactivar temporalmente las recomendaciones personalizadas que tirar los pagos.

7. Pequeña lista de comprobación práctica para tu App (en forma narrativa)

En un ChatGPT App típico con MCP/Gateway, tiene sentido recorrer de forma secuencial las siguientes preguntas.

Primero, comprueba si hay timeouts en todas las llamadas externas. Todo el código que usa fetch, las solicitudes a la BD y al LLM debe usar un wrapper como fetchWithTimeout con valores adecuados. Es importante que no haya lugares donde una petición pueda quedarse colgada indefinidamente.

Después, identifica las dependencias más frágiles. Por lo general, son pasarelas de pago, ACP, grandes APIs externas y, a veces, tu propia BD de pedidos. Alrededor de ellas conviene añadir un circuit breaker para protegerse de la avalancha de repeticiones hacia un servicio claramente muerto. Al mismo tiempo, decide de antemano cómo debe comportarse ChatGPT cuando el breaker esté en estado open.

A continuación, mira tus recursos como «compartimentos». ¿Pasa todo por el mismo connection pool y el mismo pool de workers, o las operaciones críticas (login, checkout) tienen sus propios límites de paralelismo, independientes del servicio de recomendaciones y de analítica? Si no, añade una implementación básica de bulkheads, aunque sea como un límite sencillo de tareas concurrentes.

Por último, audita todos los webhooks entrantes. Comprueba si tienen idempotency key o eventId, si no estás intentando hacer trabajo pesado de forma síncrona en el manejador HTTP y si puedes sobrevivir a una oleada de reintentos si tu downstream cae temporalmente. Si no, mueve la lógica a una cola y a workers en background.

Esta secuencia de pasos aporta una mejora muy notable de la resiliencia incluso sin infraestructura súper compleja.

8. Errores típicos con timeouts, circuit breakers, bulkheads y tormentas de webhooks

Error n.º 1: falta de timeouts «en algún punto inferior».
A menudo los desarrolladores ponen timeout solo en el Gateway o solo en el frontend, olvidando que dentro del backend hay BD, APIs externas y LLM. Al final, la solicitud externa parece tener un timeout de 5 segundos, pero por dentro una llamada a la BD o a la pasarela puede quedarse colgada minutos, bloqueando el pool de conexiones y provocando fallos en cascada.

Error n.º 2: timeouts gigantes «por si acaso».
A veces se pone un timeout de 60120 segundos: «que llegue como sea». En el contexto de ChatGPT esto casi siempre es malo. El usuario se va, el modelo empieza a inventar, y tus recursos están bloqueados todo ese tiempo. Mucho mejor un rechazo honesto a los 510 segundos con una explicación clara.

Error n.º 3: circuit breaker sin UX pensado.
A veces se añade el breaker «por cumplir», pero cuando actúa, al usuario o al modelo le llega un 500 incomprensible, «ECONNREFUSED» o «axios error». Al final GPT no puede explicar bien qué pasa y empieza a inventar. Conviene diseñar desde el principio textos de error comprensibles para personas y para el modelo.

Error n.º 4: mezclar recursos sin enfoque de bulkhead.
Escenario clásico: un servicio de recomendaciones (o de analítica) empieza a ir lento, se come todo el pool de conexiones a la BD o el thread-pool, y detrás caen checkout y login. Todo porque los recursos no están separados. La ausencia de cualquier enfoque de bulkhead provoca que una función secundaria tumbe todo el prod.

Error n.º 5: tratar webhooks como peticiones normales.
Los principiantes a menudo escriben el manejador de webhooks igual que un controlador normal: lógica de negocio larga, llamadas a APIs de terceros, sin idempotencia. Con reintentos y duplicados, esto lleva al doble procesamiento de eventos, estados extraños de pedidos y caídas por carga durante la tormenta.

Error n.º 6: ignorar la idempotencia en escenarios de commerce.
Es especialmente peligroso cuando un webhook de pago puede crear un pedido otra vez o cambiar su estado de nuevo. Sin verificar el idempotency key y guardar el estado de procesamiento del evento, tarde o temprano tendrás doble cobro o duplicados extraños de pedidos.

Error n.º 7: intentar arreglarlo todo con setTimeout y «retrasos mágicos».
A veces se quiere sortear condiciones de carrera y problemas de tormenta con «esperar 100 ms y listo». En la práctica hace el comportamiento aún más inestable y no protege de fallos reales. El camino correcto son timeouts explícitos, circuit breaker, colas e idempotencia, no magia con retrasos.

Error n.º 8: falta de priorización de rutas críticas.
Cuando el checkout y el login viven con los mismos límites que la analítica o la lógica de recomendación, cualquier sobrecarga puede tumbar por igual partes críticas y secundarias. En un diseño resiliente, checkout y auth son «vacas sagradas»: recursos separados, límites separados, alertas y SLO separados.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION