CodeGym /Cursos /ChatGPT Apps /Gateway y defensa del perímetro: proxy, rate limiting, co...

Gateway y defensa del perímetro: proxy, rate limiting, colas y backpressure

ChatGPT Apps
Nivel 16 , Lección 1
Disponible

1. ¿Por qué proteger el perímetro de una ChatGPT App?

En una aplicación web clásica el usuario es el navegador, que golpea tus endpoints de forma bastante predecible. En el mundo de las ChatGPT Apps aparece un nuevo tipo de cliente: una LLM que decide por sí misma cuándo y qué herramientas invocar.

El modelo puede:

  • en un mismo diálogo invocar varias veces seguidas la misma tool;
  • experimentar: «¿y si volvemos a llamar a suggest_gifts con parámetros ligeramente diferentes?»;
  • trabajar en paralelo para cientos de usuarios.

Añade bots potenciales, scripts de prueba y errores en tu propio código (por ejemplo, un bucle infinito que dispara constantemente un tool‑call) y tendrás casi la receta perfecta de un DoS bienintencionado.

La guinda del pastel: el coste. Cada tool‑call puede:

  • tirar de APIs externas de pago (mensajería, pagos, catálogos),
  • invocar otras LLM (por ejemplo, búsqueda RAG),
  • lanzar tareas pesadas en background.

Sin límites ni defensa perimetral, un cliente «desafortunado» puede:

  • tumbar todos tus servicios backend tras el gateway (Gift API, Commerce API, etc.),
  • reventar los límites de APIs externas,
  • y quemar de forma notable el presupuesto de modelos.

El objetivo de esta lección es mostrar cómo gateway/proxy + rate limiting + colas + backpressure convierten esa catástrofe potencial en un sistema controlable.

Insight

La plataforma de ChatGPT no proporciona ningún mecanismo de protección de tu servidor MCP frente a tráfico externo. Cualquier cliente de Internet puede enviarle solicitudes, incluidas utilidades como MCP Jam.

Lo único que te puede ofrecer ChatGPT es limitar el tráfico entrante por direcciones IP, configurando un reverse proxy (por ejemplo, NGINX) para trabajar con una allowlist. Si no configuras el filtrado por IP, tu servidor MCP queda completamente abierto, lo cual es inseguro. Ni para ti ni para tus usuarios.

2. Proxy/Gateway como «escudo» delante de los servicios backend y los agentes

Primero, recordemos el diagrama, pero ahora con la lente de la defensa.

Imagina un esquema típico:

flowchart LR
  ChatGPT["ChatGPT / Widget"]
    --> GW["MCP Gateway (Auth, Rate Limit, Logs)"]

  GW --> GiftAPI["Gift REST API (selección de regalos)"]
  GW --> CommerceAPI["Commerce REST API (checkout, ACP)"]
  GW --> Analytics["Analytics Service / REST API"]

  GW --> Queue["Cola de tareas"]
  Queue --> Worker["Trabajadores en segundo plano"]

El Gateway se sitúa entre el mundo exterior (ChatGPT, webhooks, clientes de prueba) y todo lo demás. Este:

  • ve absolutamente todas las solicitudes entrantes;
  • verifica primero el token y el formato de la solicitud;
  • puede descartar cosas claramente imposibles (host falso, path extraño, body demasiado grande);
  • decide a qué servicio interno REST/HTTP tiene sentido enviar la solicitud.

En ese mismo nivel aparecen:

  • rate limiting — limitamos cuántas solicitudes se pueden hacer por intervalo de tiempo;
  • backpressure básico — rechazamos si los servicios subyacentes ya van ahogados;
  • paso a asincronía — las cosas pesadas van directamente a la cola; respuesta al cliente: «recibido, esperando».

Es decir, el gateway no es solo un «router», sino un «chaleco antibalas». Lo importante es no convertirlo en «el monolito del negocio», de eso ya hablamos en la lección anterior.

3. Qué flujos de tráfico hay que controlar

En el ecosistema de una ChatGPT App suele haber tres tipos principales de tráfico que nos interesan desde el punto de vista de límites y protección.

En primer lugar, están los MCP tool‑calls desde ChatGPT. Es todo lo que llega por el protocolo MCP: llamadas a suggest_gifts, get_product_details, create_checkout_session y otras herramientas. El modelo puede generarlas con bastante alegría, especialmente si por debajo hay Agents.

En segundo lugar, están las solicitudes salientes de nuestros backends a APIs externas. Dentro del servicio podemos tener nuestros propios rate limits hacia sistemas de terceros: catálogos, logística, pagos. Romperlos implica bloqueos, sanciones o menor calidad.

En tercer lugar, están los webhooks entrantes — notificaciones de ACP, proveedores de pago (Stripe, etc.), mensajería. Llegan independientemente de la actividad de los usuarios. Si nuestro endpoint se ralentiza o responde con error, el sistema externo empieza a hacer reintentos y puede montarte una «tormenta» de notificaciones repetidas.

Para GiftGenius se ve así:

  • el usuario y el modelo invocan activamente suggest_gifts y find_similar_gifts;
  • el checkout‑tool llama al backend comercial/ACP;
  • tras el pago, el proveedor envía webhooks payment.succeeded / payment.failed.

Todos estos flujos confluyen en un punto — el Gateway, y por tanto es razonable colocar allí «contadores, filtros y embudos».

4. Rate limiting: protección básica y ahorro de dinero

Qué es el rate limiting en nuestro contexto

El rate limiting es un mecanismo que limita el número de solicitudes de un cliente concreto por unidad de tiempo. La idea es tan vieja como Internet, pero en el contexto de ChatGPT Apps resuelve de golpe tres problemas:

  • evita que un solo cliente (o bug) tumbe tus servicios;
  • ayuda a respetar los límites de APIs externas;
  • protege tu bolsillo de llamadas incontroladas a modelos.

Algoritmos clásicos:

  • ventana fija (Fixed Window),
  • ventana deslizante (Sliding Window),
  • cubo de fichas (Token Bucket),

nos interesa más a nivel conceptual: «en un minuto, no más de N solicitudes», «cada solicitud consume un token, los tokens se reponen a velocidad X por segundo», etc. La implementación suele delegarse a una librería o a un API Gateway.

Dónde colocar los límites

Los límites pueden colocarse en distintos niveles.

A nivel de reverse proxy (Nginx, Cloudflare, AWS API Gateway) es útil:

  • recortar el tráfico más salvaje por IP;
  • limitar el tamaño del cuerpo de la solicitud;
  • protegerse ante patrones de DDoS simples.

A nivel de MCP Gateway (aplicación) conviene hacer un rate limiting más «inteligente»:

  • por usuario (userId del token),
  • por organización (tenantId),
  • por tipo de operación (por ejemplo, limitamos de forma estricta create_checkout_session, search — más suave),
  • por origen (webhook vs tool‑call).

Y aparte, se pueden añadir límites dentro de los propios microservicios para operaciones especialmente costosas, pero eso es otro nivel de detalle.

Cómo elegir la clave para los límites

El error más común es limitar por dirección IP. En el caso de ChatGPT resulta bastante inútil:

  • todas las solicitudes pueden venir de un mismo rango de OpenAI,
  • usuarios distintos «se sentarán» detrás de la misma IP.

Nos interesan mucho más:

  • userId — el usuario concreto en tu aplicación;
  • tenantId — la organización (si haces B2B y un chat lo usan varios empleados);
  • un token de API o clientId, si tienes varias integraciones.

En GiftGenius suele bastar con userId + tenantId, extraídos del token que ChatGPT pasa en las llamadas MCP.

Implementación sencilla de rate limiting en TypeScript

Imaginemos que tenemos un pequeño MCP Gateway en Express. Añadimos un rate limiting básico: no más de 30 tool‑calls por minuto por usuario.

// Limitación de velocidad primitiva: N solicitudes por minuto por userId
const WINDOW_MS = 60_000;
const MAX = 30;
const hits = new Map<string, { ts: number; count: number }>();

function rateLimit(req: Request, res: Response, next: NextFunction) {
  const userId = (req.headers["x-user-id"] as string) ?? "anonymous";
  const now = Date.now();
  const rec = hits.get(userId) ?? { ts: now, count: 0 };

  if (now - rec.ts > WINDOW_MS) {      // La ventana "caducó": empezamos de nuevo
    rec.ts = now;
    rec.count = 0;
  }
  rec.count += 1;
  hits.set(userId, rec);

  if (rec.count > MAX) {
    return res.status(429).json({
      error: "rate_limit_exceeded",
      retryAfterSec: 60,
      message: "Too many tool calls, please retry later."
    });
  }
  next();
}

Y ahora lo usamos en la ruta MCP:

// Aplicamos el middleware a todas las llamadas de herramienta MCP
app.post("/mcp/tools/call", rateLimit, async (req, res) => {
  const result = await callBackendForTool(req.body); // Llamada REST a Gift/Commerce/Analytics API
  res.json(result);
});

Puntos clave:

  • devolvemos un error con sentido (error: "rate_limit_exceeded"), y no simplemente un 500;
  • el modelo podrá leer ese error, entender qué ocurrió y explicárselo correctamente al usuario, en lugar de empezar a alucinar.

En producción real, los contadores viven obviamente fuera de la memoria de un único proceso, en Redis u otro almacén compartido, para que todo funcione en clúster. Pero para comprender el principio, esto basta.

El rate limiting y los límites a nivel de gateway nos protegen de la avalancha de solicitudes, pero no resuelven otro problema: hay operaciones que aun así pueden ser muy pesadas y tardar mucho. Aquí el HTTP síncrono no basta, y entran en escena las colas y las tareas asíncronas.

5. Colas y tareas asíncronas: cuando ya no se puede en síncrono

El problema de los timeouts en ChatGPT

Aunque configures con mimo el rate limiting, a ChatGPT (y a los clientes HTTP en general) no le gusta cuando la respuesta tarda demasiado. La plataforma limita el tiempo de ejecución de un tool‑call y, si esperas a que termine algún algoritmo «súper‑recomendador», entonces:

  • el usuario verá un spinner eterno;
  • la plataforma cortará la solicitud por timeout;
  • el modelo pensará que «algo ha ido mal» y empezará a inventarse explicaciones.

La solución: mover las operaciones pesadas a modo asíncrono. Patrón clásico:

  1. El Gateway recibe la solicitud.
  2. Coloca la tarea en una cola.
  3. Devuelve inmediatamente una respuesta 202 Accepted con jobId.
  4. Un worker aparte recoge tareas de la cola y las procesa.
  5. El cliente (nuestro widget o incluso ChatGPT mediante otra tool) pregunta periódicamente el estado por jobId o recibe una notificación vía evento MCP.

En términos de ChatGPT App, esto suele verse como dos herramientas: la primera tool recibe la solicitud, mete la tarea en la cola y devuelve el jobId; la segunda permite al modelo o al widget consultar el estado con ese jobId y recoger el resultado. Además, puedes duplicar eventos de progreso por notificaciones MCP.

Mini‑cola para GiftGenius (ejemplo de código)

Supongamos que tenemos una herramienta pesada generate_large_gift_report, que puede tardar decenas de segundos. En una App real podría devolver solo el jobId, y una tool aparte get_report_status permitiría al modelo o al widget consultar el estado y obtener el resultado con ese jobId. A nivel de Gateway le hacemos un endpoint con cola.

type Job = { id: string; payload: any };
const queue: Job[] = [];
const MAX_QUEUE = 100;

app.post("/mcp/tools/generate_report", (req, res) => {
  if (queue.length >= MAX_QUEUE) {
    return res.status(503).json({
      error: "system_busy",
      message: "System is busy, please retry later."
    });
  }

  const job: Job = { id: crypto.randomUUID(), payload: req.body };
  queue.push(job);
  res.status(202).json({ jobId: job.id, status: "accepted" });
});

Y un worker primitivo que cada 200 ms toma una tarea:

async function processJob(job: Job) {
  // Aquí llamamos al servicio backend real o a un flujo de trabajo de agentes vía REST
  await handleHeavyGiftReport(job.payload);
}

setInterval(async () => {
  const job = queue.shift();
  if (!job) return;
  await processJob(job);
}, 200);

Está claro que es un ejemplo muy simplificado:

  • en la vida real la cola vive en Redis, SQS, Kafka, etc.;
  • el estado de la tarea se almacena en otro sitio, para poder consultarlo;
  • suele haber varios workers.

Pero el concepto ya está claro: el Gateway no mantiene la solicitud abierta hasta que todo termine. Recibe, pone en trabajo y responde rápido.

6. Backpressure: cómo no ahogarse en tu propia cola

En qué se diferencia el backpressure del rate limiting

El rate limiting responde principalmente a la pregunta: «¿cuántas solicitudes puede hacer un cliente por intervalo de tiempo?». Es defensa contra «un usuario demasiado activo» o un bug del lado de un cliente concreto.

El backpressure dice: «¿y cuántas tareas/solicitudes en total es capaz de digerir nuestro sistema a la vez sin romperse?». Se trata del volumen global de carga, independientemente de quién la envió.

Ejemplo:

  • rate limiting: «el usuario no puede invocar suggest_gifts más de 30 veces por minuto»;
  • backpressure: «no puede haber más de 100 tareas pendientes en la cola; si no, empezamos a rechazar nuevas solicitudes».

En el ideal, estos mecanismos se complementan: el rate limit mantiene a raya a los clientes; el backpressure salva al sistema si, aun así, llega demasiada gente a la vez.

Implementación simple de límite de tareas activas

Una de las variantes más sencillas de backpressure es limitar el número de llamadas activas por debajo. Por ejemplo: no mantener más de 50 tool‑calls activos simultáneamente hacia un servicio backend/REST concreto (Gift API, Commerce API, etc.).

let activeCalls = 0;
const MAX_ACTIVE = 50;

app.post("/mcp/tools/call", async (req, res) => {
    if (activeCalls >= MAX_ACTIVE) {
        return res.status(429).json({
            error: "gateway_overloaded",
            message: "Gateway is temporarily overloaded, please retry later."
        });
    }

    activeCalls += 1;
    try {
        const result = await callBackendForTool(req.body); // Llamada REST a Gift/Commerce/Analytics API
        res.json(result);
    } catch (err) {
        console.error("Tool call error", err);
        res.status(500).json({ error: "internal_error" });
    } finally {
        activeCalls -= 1;
    }
});

Qué está pasando aquí:

  • mientras el número de solicitudes en ejecución simultánea sea menor que MAX_ACTIVE, dejamos pasar la nueva llamada;
  • si el límite se agota, respondemos inmediatamente con un error con sentido;
  • es importante disminuir el contador en el finally para no perder «slots» en caso de error.

Esto es el backpressure más simple: decimos honestamente al cliente: «no puedo ahora, inténtalo más tarde», en lugar de aceptar todo sin pensar y morir.

Más adelante puedes:

  • definir diferentes MAX_ACTIVE para distintos tipos de operaciones (por ejemplo, casi siempre dejar pasar el checkout y limitar más la generación de informes),
  • cambiar los límites de forma dinámica según las métricas de carga.

7. Webhooks y «tormentas»: protección de eventos entrantes

Hasta ahora miramos sobre todo las solicitudes que iniciamos nosotros o ChatGPT (tool‑calls, solicitudes salientes, async‑jobs). Pero en la vida hay otra fuente importante de carga sobre el Gateway — los webhooks entrantes desde sistemas externos.

Los webhooks son la otra cara de la moneda: si los tool‑calls los iniciamos nosotros (vía el modelo), los webhooks los inicia el servicio externo. Es ese tercer tipo de tráfico del apartado 4 que no controlamos en tiempo ni frecuencia, pero que debemos procesar sin caernos. Pagos, ACP, logística — todos envían notificaciones (webhooks) a nuestro endpoint ante cada cambio relevante: «pago realizado», «pedido creado», «la entrega ha actualizado su estado».

Los problemas empiezan cuando:

  • nuestro endpoint responde lento;
  • responde con error;
  • está indisponible de forma intermitente.

Entonces el servicio externo, siguiendo buenas prácticas, empieza a reintentar. Y con mala suerte, recibirás una «tormenta de webhooks»: decenas o cientos de eventos repetidos intentando «contactar» contigo a toda costa.

Para no morir de tanta «atención», a nivel de Gateway conviene:

  1. Limitar los webhooks entrantes por origen: por ejemplo, «no más de 10 eventos por minuto para un event_type concreto de un proveedor».
  2. Verificar la firma antes de parsear JSON: la firma HMAC o mecanismo análogo permite descartar solicitudes falsas.
  3. Hacer la gestión de eventos idempotente: por event_id o campo similar, para que los repetidos no creen pedidos o pagos duplicados.
  4. Ante una tormenta fuerte, activar backpressure adicional: responder temporalmente «503: inténtelo más tarde» si los servicios downstream no dan abasto.

Ejemplo sencillo (idea, no código de producción):

app.post("/webhooks/stripe", rateLimitWebhook, (req, res) => {
    const sig = req.headers["stripe-signature"] as string;
    if (!isValidSignature(req.rawBody, sig)) {
        return res.status(400).send("Invalid signature");
    }

    const event = JSON.parse(req.body.toString());
    if (isAlreadyProcessed(event.id)) {
        return res.json({ received: true }); // idempotencia
    }

    handleStripeEvent(event);
    res.json({ received: true });
});

Aquí, a nivel de Gateway, nosotros:

  • aplicamos una política de rate limiting distinta para webhooks;
  • validamos la firma antes de confiar en el contenido;
  • nos protegemos de duplicados con isAlreadyProcessed.

8. Lo aplicamos a GiftGenius: ejemplo de política de límites y colas

Dejemos las abstracciones y veamos cómo podría verse en nuestro GiftGenius didáctico.

Imaginemos tres escenarios clave:

  1. Búsqueda de regalos (suggest_gifts, find_similar_gifts).
  2. Creación de pedido / checkout (create_checkout_session, confirm_order).
  3. Recepción de webhooks del proveedor de pago y de ACP.

Para cada escenario, tiene sentido definir:

  • con qué clave contamos el límite;
  • cuántas solicitudes por minuto permitimos;
  • qué hacemos al excederlo.

Por ejemplo:

Escenario Clave de límite Límite por minuto Comportamiento al exceder
Búsqueda de regalos userId 30 429 + consejo «reducir los parámetros de búsqueda»
Creación de pedido userId + tenantId 5 429 + texto «demasiados intentos, revise los pedidos»
Webhooks entrantes provider + eventType 10 429/503, log, posible degradación

Para webhooks, suele ser más lógico limitar por la combinación «proveedor + tipo de evento» y filtrar duplicados con un mecanismo de idempotencia por event_id.

En código, esto se traduce en distintos middlewares: rateLimitSearch, rateLimitCheckout, rateLimitWebhook.

Para operaciones pesadas —como «generar un informe PDF grande de regalos del año»— usamos la cola y el patrón asíncrono mostrado arriba. En ese caso, el Gateway:

  • recibe la solicitud desde ChatGPT;
  • pone la tarea en la cola;
  • devuelve el jobId y una pista para el modelo de cómo obtener el estado;
  • limita el tamaño de la cola (backpressure) para no desbordar el sistema.

Es importante recordar: el rate limiting y el backpressure no son solo seguridad y fiabilidad, también son UX. Es mucho más agradable que el asistente diga: «El servicio está saturado, probemos en un minuto», que quedarse mirando un spinner hasta el timeout o ver un «Internal Server Error».

9. Mini‑práctica: añadimos protección a nuestro MCP Gateway

Para que el material no se quede en teoría, vamos a montar una mini‑práctica que puedes implementar en tu proyecto de aprendizaje.

Rate limiting para todos los MCP tool‑calls

Añade el middleware rateLimit (como arriba) y conéctalo a /mcp/tools/call. Para empezar, puedes usar un límite muy simple: 30 solicitudes por minuto por userId. Luego juega un poco:

  • reduce el límite y observa cómo reacciona tu App y el modelo;
  • establece límites distintos para distintos tipos de tools pasando, por ejemplo, toolName al middleware.

Backpressure básico por llamadas activas

Añade el contador activeCalls y el límite MAX_ACTIVE. Intenta simular carga (por ejemplo, con un script que envíe un lote de solicitudes) y observa en qué momento el Gateway empieza a responder con gateway_overloaded.

Aquí lo importante es el comportamiento: no esperas a que todo se caiga, sino que dejas de aceptar nuevas tareas, diciendo honestamente al cliente que ahora mismo está muy caliente.

Cola para una herramienta pesada

Elige una operación pesada (o hazla «pesada» artificialmente — metiendo un setTimeout/un fetch largo) y pásala al patrón «cola + jobId». Mínimo:

  • endpoint POST /mcp/tools/generate_report — mete la tarea en la cola y devuelve jobId;
  • endpoint GET /jobs/:id — devuelve el estado (pending, done, error y, posiblemente, el resultado);
  • un worker que cada X milisegundos llame a processJob.

Con eso basta para entender cómo se verá la integración real con BullMQ u otro motor de colas.

10. Errores típicos al proteger el perímetro

Error nº 1: Limitar solo por IP.
En el mundo de las ChatGPT Apps es casi inútil: la mayoría de solicitudes llegan desde direcciones de OpenAI, y todos tus usuarios acabarán detrás de la misma IP. Al final, alguien quemará el límite para todos y el verdadero culpable quedará oculto. Es más correcto limitar por userId, tenantId o por token, y usar la IP solo como filtro muy grueso a nivel de reverse proxy.

Error nº 2: Devolver un simple 500 en lugar de un error con sentido.
Si, al rebasar el límite o por sobrecarga, simplemente envías un 500 Internal Server Error, el modelo no entiende nada y empieza a inventar. En cambio, un error estructurado con un código (rate_limit_exceeded, gateway_overloaded) y una descripción legible permite a la LLM explicar la situación correctamente al usuario y, si procede, reintentar más tarde.

Error nº 3: Hacer una cola infinita sin backpressure.
A veces parece: «metamos todo en la cola y ya veremos». En la práctica, la cola crece a miles de tareas, las demoras aumentan, la memoria se agota y los usuarios no ven el resultado. Limita siempre el tamaño de la cola y el número de operaciones activas. Es mejor rechazar honestamente nuevas solicitudes con 503 o 429 que convertir la cola en un agujero negro.

Error nº 4: Confiar solo en rate limiting e ignorar los webhooks.
Muchos protegen solo el tráfico entrante de ChatGPT y dejan los webhooks «ya se verá». Cuando el proveedor de pagos empieza con reintentos, son precisamente los webhooks los que pueden montarte una auténtica tormenta. Los endpoints de webhooks necesitan sus propios límites, verificación de firma e idempotencia en el procesamiento. Si no, es fácil recibir una decena de duplicados del mismo pedido.

Error nº 5: Guardar todos los contadores y la cola solo en la memoria de una instancia.
Para un proyecto de aprendizaje está bien, pero en producción, al escalar el Gateway a varias instancias, los contadores en cada nodo «viven su vida», los límites dejan de ser globales y reiniciar un nodo vacía la cola. En un sistema real, para almacenar el estado de límites y colas se usa un almacén común (Redis, colas en la nube, etc.). Hablaremos de ello en las lecciones sobre escalado y producción.

Error nº 6: Meter lógica de negocio dentro del Gateway «ya que es el intermediario para todo».
A veces tienta: «resolvamos en el Gateway qué regalos mostrar; total, ahí llegan las solicitudes». Al final, el gateway se convierte en un monolito con un montón de lógica que es a la vez router, cerebro de negocio y logger. Eso complica mucho el escalado y el mantenimiento. El Gateway debe permanecer como capa de red/infraestructura: autenticación, autorización, límites, caché, enrutamiento — sí; elección de regalos — no.

Error nº 7: Pensar «somos pequeños, esto no va con nosotros».
Se suele pensar: «No tenemos un millón de usuarios, podemos pasar sin gateway/límites». En realidad, incluso un único bug en el código cliente (o en un prompt que haga que el modelo llame a una tool en bucle) puede montarte un pequeño pero muy local apocalipsis. Un rate limiting básico y, al menos, un backpressure primitivo no son un lujo, son el cepillo de dientes de la producción: hay que usarlos desde el principio, antes de que empiece el dolor.

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