SenseiTeoría desde cero0/27

4 · La red y la webLección 2 de 6

HTTP: requests, respuestas y códigos

HTTP: requests, responses and status codes

Qué dice, letra por letra, el mensaje que lleva la foto de Ana hasta el modelo y qué le contestan: la URL, el método, los headers, el body y el código de tres cifras que decide si alguien reintenta, cae a su plan B o se rinde.

⏱ 58 min lo esencial · 73 min todo11 conceptos nuevos2 calculadoras9 ejerciciosAPI
Secciones14
  1. 01Un mensaje de texto con forma fija
  2. 02La primera línea: método y URL
  3. 03Headers: lo que va escrito en el sobre
  4. 04El body: dónde viaja la foto
  5. 05Códigos de estado: qué pasó y de quién es la culpa
  6. 06Leer una request sin confiar en ella extra
  7. 07HTTPS, keep-alive y pools: no pagues dos veces la conexión
  8. 08HTTP/2 y HTTP/3 extra
  9. 09Errores comunes
  10. 10En la vida real extra
  11. 11Ejercicios
  12. 12Lo que sigue
  13. 13Palabras que vas a oír
  14. 14Chequeo
La pregunta

¿Qué dice exactamente el mensaje que llega al gateway de C01 con la foto de Ana, qué le contesta el gateway y qué significa cada número de la respuesta?

Ana toma la foto de sus tenis y toca «Publicar». En la lección anterior viste cómo viaja un mensaje de una máquina a otra: con una dirección IP, un puerto y una conexión TCP que entrega los bytes completos y en orden. Pero TCP solo mueve bytes. No dice qué significan.

¿Cómo sabe el gateway de C01 (el programa que recibe la foto, la prepara y se la pasa al modelo) que esos 214,836 bytes son una foto, que alguien quiere su categoría y que espera cinco sugerencias? Lo sabe porque los dos programas hablan el mismo idioma: HTTP.

El camino es así. La app de Ana sube la foto al backend del marketplace, el programa que recibe las publicaciones. El backend le manda la foto al gateway con una request de HTTP, y el gateway contesta con una respuesta de HTTP. Las dos son texto que puedes leer.

En esta lección las abres pieza por pieza: la URL, el método, los headers, el body y el código de estado. Vas a levantar un servidor en tu computadora y a hacerle requests de verdad. Y al final vas a poder leer cualquier línea de la Ronda que diga «POST /v1/classify?top_k=5» o «503 con Retry-After» sabiendo qué dice cada pedazo.

01

Un mensaje de texto con forma fija

Esencial

A text message with a fixed shape

Un protocolo es un conjunto de reglas que dos programas acuerdan para entenderse: quién habla primero, qué forma tiene cada mensaje y qué significa cada parte. HTTP (HyperText Transfer Protocol, protocolo de transferencia de hipertexto) nació con la web, a principios de los noventa (Tim Berners-Lee lo escribió en el CERN, el laboratorio europeo de física, entre 1989 y 1991), para pedir páginas. Hoy es el idioma por omisión entre programas: apps, APIs, servidores de modelos.

HTTP tiene dos papeles. El cliente pide y el servidor responde. Siempre empieza el cliente, y cada request recibe exactamente una respuesta. En C01, el backend es el cliente y el gateway, el servidor. Pero el gateway es cliente a su vez cuando le pide la predicción a Triton, el servidor del modelo. Ser cliente o servidor no depende de la máquina: depende de quién pide en esa conversación.

La request, letra por letra

Esto es lo que el backend le escribe al gateway por la conexión TCP:

text
POST /v1/classify?top_k=5 HTTP/1.1
Host: classifier.internal
Authorization: Bearer eyJhbGciOi...
Content-Type: image/jpeg
Content-Length: 214836
X-Request-ID: 9f2c41d0

(aquí siguen los 214,836 bytes de la foto)

Tiene cuatro partes, siempre en este orden:

  1. 1

    La línea de la petición

    Request line. Tres cosas separadas por espacios: el método (POST, qué quieres hacer), la ruta con su query (/v1/classify?top_k=5, sobre qué) y la versión del protocolo (HTTP/1.1).

  2. 2

    Los headers

    Una línea por dato, con la forma Nombre: valor. Dicen a quién va dirigida, quién la manda, qué trae el body y cuánto mide.

  3. 3

    Una línea en blanco

    Marca dónde terminan los headers. Sin ella, el servidor no sabría si viene otro header o ya empezó la foto.

  4. 4

    El body

    El cuerpo: los bytes de la foto, tal cual. Puede no haber body, como en una request que solo pide algo.

Cada línea termina con dos caracteres invisibles, \r\n: carriage return y line feed (retorno de carro y salto de línea), un recuerdo de las máquinas de escribir. La línea en blanco es justo eso: un \r\n pegado al de la línea anterior, \r\n\r\n. Cuando el servidor lo encuentra, sabe que lo que sigue es el body.

¿Y dónde termina el body? TCP entrega un chorro de bytes sin orillas: detrás de esta request podría venir otra por la misma conexión. Por eso el header Content-Length dice cuántos bytes mide: el gateway lee exactamente 214,836 y sabe que la request terminó.

La respuesta

El gateway contesta con la misma forma:

text
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 345
X-Request-ID: 9f2c41d0

{
  "model_version": "2026-09-01-resnet50-fp16",
  "predictions": [
    {"label": "Calzado > Tenis", "score": 0.91},
    {"label": "Calzado > Zapatos casuales", "score": 0.04},
    {"label": "Calzado > Botas", "score": 0.02},
    {"label": "Deportes > Calzado deportivo", "score": 0.01},
    {"label": "Calzado > Sandalias", "score": 0.01}
  ]
}

La primera línea ahora es la línea de estado (status line): la versión, un código de estado de tres cifras (200) y una frase para humanos (OK). Los programas solo leen el número. Después vienen los headers, la línea en blanco y el body: un texto en JSON, un formato para escribir datos con llaves, corchetes y comillas, con las cinco categorías y la versión del modelo que contestó. Los scores son de ejemplo; la forma es la del contrato de C01 en la Ronda.

Una request de HTTP y su respuesta, con sus cuatro partes numeradas. La request: 1, la línea de la petición, POST /v1/classify?top_k=5 HTTP/1.1, con el método, la ruta con su query y la versión. 2, los headers: Host classifier.internal, Authorization Bearer y un token, Content-Type image/jpeg y Content-Length 214836. 3, una línea en blanco, que marca dónde acaban los headers. 4, el body: los 214,836 bytes de la foto. La respuesta tiene la misma forma: 1, la línea de estado, HTTP/1.1 200 OK, con la versión, el código y una frase para humanos. 2, los headers Content-Type application/json y Content-Length 345. 3, la línea en blanco. 4, el body: un JSON con la versión del modelo y 5 categorías. UNA REQUEST Y SU RESPUESTA La request: lo que manda el backend 1 POST /v1/classify?top_k=5 HTTP/1.1 método ruta y query versión 2 Host: classifier.internal Authorization: Bearer eyJ… Content-Type: image/jpeg Content-Length: 214836 3 \r\n línea en blanco: aquí acaban los headers 4 214,836 bytes de la foto, tal cual La respuesta: lo que contesta el gateway 1 HTTP/1.1 200 OK ← frase para humanos versión código 2 Content-Type: application/json Content-Length: 345 3 \r\n línea en blanco 4 {"model_version": "…-fp16", "predictions": [5 categorías]} Misma forma de ida y de vuelta; solo cambia la línea 1.
Las dos mitades de una conversación de HTTP. Las dos tienen la misma forma: una primera línea, los headers, una línea en blanco y el body. Solo cambia la primera línea: la request dice qué quiere; la respuesta, cómo le fue.

HTTP no tiene memoria

Cada request es independiente: el servidor no recuerda la anterior. Por eso cada una trae todo lo que hace falta, hasta quién la manda (Authorization). Se dice que HTTP es stateless, sin estado.

Parece un defecto y es una ventaja. Si ninguna request depende de la anterior, cualquier copia del gateway puede atender cualquier request. C01 corre 5 copias, o pods, del gateway, y el reparto entre ellas no tiene que fijarse en quién habló antes con quién.

Pruébalo: un servidor y una request de verdad

La biblioteca estándar de Python trae http.server, un servidor de HTTP pequeño. Sirve para aprender y para pruebas; su propia documentación avisa que no es para producción. El código levanta uno en localhost (127.0.0.1, tu propia máquina) y le escribe una request a mano, con un socket, el objeto con el que un programa manda y recibe bytes por una conexión. Así ves el texto tal cual viaja. Tres detalles para leerlo: b"..." es una cadena de bytes, no de texto, porque eso es lo que viaja; self.rfile y self.wfile son la conexión vista como dos archivos, uno para leer la request y otro para escribir la respuesta; y al final cada \r\n se imprime para que lo veas:

python
import socket, threading
from http.server import BaseHTTPRequestHandler, HTTPServer

class Gateway(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"
    def do_POST(self):
        foto = self.rfile.read(int(self.headers["Content-Length"]))
        print("el servidor leyó:", self.requestline, "+", len(foto), "bytes de body")
        cuerpo = b'{"label": "Calzado > Tenis", "score": 0.91}'
        self.send_response_only(200)              # sin Date ni Server: así la salida no cambia
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(cuerpo)))
        self.end_headers()                        # escribe la línea en blanco
        self.wfile.write(cuerpo)
    def log_message(self, *args): pass           # sin bitácora en la pantalla

servidor = HTTPServer(("127.0.0.1", 0), Gateway)   # puerto 0: el sistema elige uno libre
threading.Thread(target=servidor.serve_forever, daemon=True).start()   # atiende en otro hilo

peticion = (b"POST /v1/classify?top_k=5 HTTP/1.1\r\nHost: localhost\r\n"
            b"Content-Type: image/jpeg\r\nContent-Length: 4\r\nConnection: close\r\n"
            b"\r\n" + bytes([0xFF, 0xD8, 0xFF, 0xE0]))   # los 4 primeros bytes de un JPEG
with socket.create_connection(servidor.server_address) as s:
    s.sendall(peticion)
    respuesta = b"".join(iter(lambda: s.recv(4096), b""))
print(respuesta.decode().replace("\r\n", "\\r\\n\n"))
Salida
el servidor leyó: POST /v1/classify?top_k=5 HTTP/1.1 + 4 bytes de body
HTTP/1.1 200 OK\r\n
Content-Type: application/json\r\n
Content-Length: 43\r\n
\r\n
{"label": "Calzado > Tenis", "score": 0.91}

El servidor encontró la línea en blanco, leyó los 4 bytes que anunciaba Content-Length y contestó. El body de la respuesta mide 43 bytes: cuenta los caracteres de {"label": "Calzado > Tenis", "score": 0.91} y salen 43. El header Connection: close le pidió al servidor cerrar la conexión al terminar; en la sección de keep-alive vas a ver por qué normalmente no se pide.

02

La primera línea: método y URL

Esencial

The first line: method and URL

La primera línea dice qué quieres y sobre qué: POST /v1/classify?top_k=5 HTTP/1.1. El «sobre qué» es un pedazo de la URL.

La URL, por partes

Una URL (Uniform Resource Locator, localizador uniforme de recursos) es la dirección completa de algo en la red. La del gateway de C01, vista desde el backend, sería https://classifier.internal/v1/classify?top_k=5. Tiene cinco partes:

  • El esquema (scheme), https: con qué protocolo se habla. http o https, casi siempre.
  • El host, classifier.internal: el nombre de la máquina. El DNS lo convierte en una dirección IP antes de conectarse.
  • El puerto, que aquí no aparece. Si falta, se usa el de omisión: 80 para http y 443 para https. Si el servidor escucha en otro, se escribe después de dos puntos: Triton recibe HTTP en http://triton:8000.
  • La ruta (path), /v1/classify: qué cosa, dentro del servidor. Parece la ruta de un archivo, pero no tiene por qué haber un archivo: es texto que el servidor interpreta. El /v1 es la versión del contrato de la API, que se presenta en la lección siguiente como versionado.
  • La query (query string), top_k=5: parámetros opcionales, después del ?, en pares nombre=valor separados por &.
La URL https://classifier.internal:443/v1/classify?top_k=5 partida en cinco piezas de colores. El esquema, https, dice con qué protocolo se habla. El host, classifier.internal, dice a qué máquina; el DNS lo convierte en una IP. El puerto, 443, se puede omitir: por omisión es 80 en http y 443 en https. La ruta, /v1/classify, dice qué cosa dentro del servidor. La query, ?top_k=5, trae parámetros nombre igual a valor. En la request, la ruta y la query viajan en la primera línea, POST /v1/classify?top_k=5 HTTP/1.1, y el host en el header Host. El esquema y el puerto no se escriben: ya se usaron para abrir la conexión. UNA URL, PIEZA POR PIEZA https :// classifier.internal :443 /v1/classify ?top_k=5 Esquema con qué protocolo se habla: http o https Host a qué máquina; el DNS lo vuelve una IP Puerto si falta: 80 en http y 443 en https Ruta qué cosa, dentro del servidor Query parámetros nombre=valor, separados por & La ruta y la query van en la primera línea; el host, en el header Host. El esquema y el puerto no viajan: ya abrieron la conexión.
La URL del gateway de C01, desarmada, con el puerto escrito aunque sea el de omisión. El esquema y el puerto deciden cómo conectarse; el host, a qué máquina; la ruta y la query viajan en la primera línea de la request.

Fíjate en qué viaja dónde. La ruta y la query van en la primera línea. El host va en el header Host. El esquema y el puerto ni siquiera se escriben: ya los usaste para abrir la conexión. Y si una URL trae un # (un fragmento, como #precio), esa parte nunca sale del navegador: sirve para saltar a un lugar de la página.

Una URL solo admite ciertos caracteres. Los demás se escriben con % y un código: un espacio es %20.

Para ir más hondoLos caracteres especiales, y la URL desarmada en Python

Los espacios, la ñ o un & dentro de un valor se escriben con % y su código en hexadecimal. A eso se le llama percent-encoding. La ñ ocupa 2 bytes en UTF-8, la forma en que se guardan las letras como bytes, así que se vuelve %C3%B1. Python lo hace por ti:

python
from urllib.parse import parse_qs, quote, urlencode, urlsplit

for url in ("https://classifier.internal/v1/classify?top_k=5", "http://localhost:8000/v2/health/ready"):
    u = urlsplit(url)
    print(u.scheme, "·", u.hostname, "· puerto:", u.port, "·", u.path, "·", parse_qs(u.query))

print(urlencode({"q": "tenis rojos", "talla": "26.5", "marca": "Niño & Co"}))
print(quote("/buscar/tenis niño"))
Salida
https · classifier.internal · puerto: None · /v1/classify · {'top_k': ['5']}
http · localhost · puerto: 8000 · /v2/health/ready · {}
q=tenis+rojos&talla=26.5&marca=Ni%C3%B1o+%26+Co
/buscar/tenis%20ni%C3%B1o

El None dice que la URL no trae puerto: se usará el 443. En una query, el espacio se puede escribir como +; en una ruta, como %20.

Los métodos: el verbo de la request

El método dice qué quieres hacer con la ruta. Los cinco que vas a ver:

  • GET: dame esto. No lleva body. Lo usa tu navegador para cada página.
  • POST: toma esto y procésalo. Lleva body. Puede crear algo nuevo cada vez.
  • PUT: deja esto exactamente así. Manda la versión completa y reemplaza la que hubiera.
  • PATCH: cambia solo estas partes.
  • DELETE: borra esto.

Hay otros dos que aparecen de vez en cuando: HEAD, igual que GET pero sin body en la respuesta (solo los headers), y OPTIONS, que pregunta qué métodos acepta una ruta.

Los métodos se agrupan por dos propiedades. Un método es seguro (safe) si no cambia nada en el servidor: pedir no tiene efectos. Y es idempotente si hacerlo dos veces deja al servidor igual que hacerlo una. La idempotencia se presenta a fondo en la lección «Timeouts, reintentos e idempotencia», porque decide qué se puede reintentar sin miedo. Aquí basta la idea.

Método¿Lleva body?¿Seguro?¿Idempotente?Ejemplo
GETNoSíSíGET /v1/jobs/7f3c: ¿ya terminó mi trabajo? (M02)
HEADNoSíSí¿existe este archivo y cuánto pesa?
POSTSíNoNoPOST /v1/classify (C01) · POST /v1/chat/completions (C10, un LLM que responde preguntas)
PUTSíNoSíPUT /v1/usuarios/42/foto: esta es tu foto de perfil
PATCHSíNoNoPATCH /v1/publicaciones/77 con {"precio": 899}
DELETECasi nuncaNoSíDELETE /v1/jobs/7f3c: cancela el trabajo

¿Por qué DELETE es idempotente, si la segunda vez contesta distinto? Porque la idempotencia habla del efecto en el servidor, no de la respuesta. El primer DELETE borra el trabajo y responde que salió bien; el segundo no encuentra nada y responde «no existe». Pero después de los dos, el trabajo está borrado, igual que después de uno. Con POST no pasa: dos POST /v1/publicaciones iguales crean dos publicaciones.

Clasificar una foto no cambia nada en el gateway: en la práctica es seguro y se puede repetir. ¿Por qué entonces C01 usa POST y no GET? Porque la foto tiene que ir en el body, y el estándar de HTTP no le da ningún significado al body de un GET: muchos servidores y proxies (los programas intermedios que reenvían requests) lo ignoran o rechazan la request entera. El método describe la forma de la request; que clasificar sea repetible lo dice el contrato de la API, no el verbo.

¿Y si alguien manda GET /v1/classify? La ruta existe, pero no con ese método. El gateway responde 405 Method Not Allowed, con un header Allow: POST que dice qué método sí acepta. La Ronda (C01) lo registra como un error del cliente.

03

Headers: lo que va escrito en el sobre

Esencial

Headers: what's written on the envelope

Los headers son los datos sobre el mensaje: van entre la primera línea y la línea en blanco, uno por línea, con la forma Nombre: valor. El nombre no distingue mayúsculas: Content-Type y content-type son el mismo header. Y el orden casi nunca importa.

Los que vas a ver en la Ronda

HeaderVa enQué diceEjemplo
HostRequestA qué nombre va dirigida; una misma IP puede atender muchosHost: classifier.internal
Content-TypeLas dosQué hay en el body, como tipo MIMEimage/jpeg, application/json
Content-LengthLas dosCuántos bytes mide el bodyContent-Length: 214836
AuthorizationRequestQuién eres, con una credencialAuthorization: Bearer eyJhbGciOi...
AcceptRequestQué formatos aceptas en la respuestaAccept: application/json
User-AgentRequestQué programa pidecurl/8.7.1, Python-urllib/3.14
X-Request-IDLas dosUn id para seguir la request en los logs de cada piezaX-Request-ID: 9f2c41d0
Retry-AfterRespuestaEn un 429 o un 503: cuánto esperar antes de reintentarRetry-After: 1 (segundos)
AllowRespuestaEn un 405: qué métodos sí acepta la rutaAllow: POST
LocationRespuestaDónde está lo que buscas, o lo que acabas de crearLocation: /v1/jobs/7f3c

Un tipo MIME (media type) es una etiqueta de dos partes, tipo y subtipo: image/jpeg es una foto JPEG; application/json, texto en JSON; text/event-stream, una respuesta que llega en pedazos, como el texto de un LLM mientras se escribe (SSE, en la lección siguiente). Si el gateway recibe un tipo que no sabe leer, como application/pdf, responde 415.

Authorization merece su propio párrafo. Bearer quiere decir «al portador»: quien presente este token (la credencial: un texto largo que nadie puede adivinar) es quien dice ser. El token puede ser un JWT, un sobre firmado con la identidad de quien llama, o una llave de API, una contraseña larga para programas; los dos tienen su lección en la fase de identidad. Lo importante aquí: viaja en un header, en cada request, porque HTTP no recuerda quién eres.

Headers inventados

Puedes inventar los tuyos. Por costumbre empezaban con X-; desde 2012 el estándar recomienda ya no hacerlo, pero la costumbre sigue por todas partes.

Para seguir los de la Ronda hace falta conocer a un intermediario que vas a ver mucho en C01: el ALB (Application Load Balancer), el balanceador de carga de AWS (Amazon Web Services, la nube de Amazon donde corre C01). Es el programa que recibe el tráfico de C01 y reparte las requests entre las copias del gateway. Dos headers inventados de la Ronda:

  • X-Request-ID une los logs del backend y del gateway para una misma request. Lo pone el backend; el gateway lo respeta (o inventa uno si no llega), lo escribe en cada línea de su log y lo devuelve en la respuesta. El ALB no usa ese header; agrega su propio id, X-Amzn-Trace-Id.
  • x-client-source dice si la request viene del celular o de la web. Pero lo escribe quien llama, así que puede mentir: sirve para separar métricas, nunca para decidir permisos (M15). Para eso está el token, que el ALB verifica.

Cookies, de pasada

Si HTTP no recuerda nada, ¿cómo sabe una página que ya iniciaste sesión? Con una cookie. El servidor responde con Set-Cookie: sesion=ab12; HttpOnly; Secure y el navegador la guarda. Desde entonces, cada request a ese sitio lleva Cookie: sesion=ab12, y el servidor busca esa sesión en su base de datos. La memoria no está en HTTP: está en un header que el navegador repite.

Entre servicios, como del backend al gateway, casi no se usan: cada request lleva su Authorization.

Para ir más hondoUn header y una cookie para cambiar de modelo

Cuando C01 prueba una versión nueva del modelo, la Ronda (M11) usa los dos mecanismos de esta sección. En una prueba A/B, que compara dos versiones del modelo dándole cada una a una parte de los vendedores, el backend pone el header inventado X-Model-Arm con la versión que le toca a ese vendedor, y una regla del ALB manda la request a esa versión mirando el header. Y durante un cambio gradual de versión, el ALB puede usar una cookie suya (AWSALBTG) para mandar a la misma persona siempre a la misma versión, lo que se llama una sesión pegajosa.

04

El body: dónde viaja la foto

Esencial

The body: where the photo travels

El body es todo lo que va después de la línea en blanco. Su formato lo dice Content-Type y su tamaño, Content-Length. Puede estar vacío, como en un GET.

Tres formas de mandar una foto

1. Los bytes crudos. El body es el archivo JPEG tal cual, con Content-Type: image/jpeg. Es lo que hace C01. Lo que llega es exactamente lo que se mandó: nada que traducir, nada que crezca. El código de referencia de la Ronda lo justifica en un comentario: menos trabajo para leerlo y ningún archivo temporal.

2. JSON con base64. Un body en JSON es texto, y un texto no puede llevar bytes cualquiera: el byte 0, por ejemplo, no es una letra. La solución es base64, que reescribe cada 3 bytes como 4 letras de un alfabeto de 64 símbolos seguros (A–Z, a–z, 0–9, + y /). El body queda así:

text
{"image": "/9j/4AAQSkZJRgABAQ...", "top_k": 5}

Ese /9j/ del principio es como se escriben en base64 los primeros tres bytes de cualquier JPEG, que en hexadecimal son FF D8 FF. El precio es el tamaño:

La foto de Ana, en bytes crudos y en base64La cuenta
Bytes de la fotola del celular, ya reducida a ≤ 1024 px; la de Ana, 214,836 B, redondeada
—
200,000 B
Grupos de 3 bytesel último incompleto también cuenta
200,000 ÷ 3
66,667
Letras en base644 por cada grupo
66,667 × 4
266,668 B
Crecimiento
266,668 ÷ 200,000
1.33: un 33 % más
Tráfico al pico de 500 fotos por segundo, crudas
200,000 × 500 × 8 bits
0.8 Gb/s
Tráfico al pico, en base64
266,668 × 500 × 8 bits
≈ 1.07 Gb/s
Resultado
Base64 cuesta un tercio más de bytes en cada request, y además CPU para codificar de un lado y decodificar del otro. Por eso C01 manda los bytes crudos.

La Ronda (M07) hace la misma cuenta con el tensor que el gateway le manda a Triton, la tabla de números ya lista para el modelo: 3 × 224 × 224 números de 4 bytes, 602,112 bytes en binario, ≈ 803 KB en base64 y ≈ 3.1 MB si los números van como texto en JSON. JSON sirve para leer y depurar; para mover muchos bytes, no.

3. multipart/form-data. Es la forma en que un formulario web sube archivos. El body se parte en secciones separadas por una frontera, una línea larga y única que no aparece en los datos. Cada sección trae sus propios mini-headers: una lleva la foto en bytes crudos y otra, por ejemplo, el título de la publicación. Agrega unos cientos de bytes, no un tercio. Conviene cuando la foto viaja con otros campos.

El tope: el 413 casi gratis

Un body sin tope es una invitación al abuso. Una foto de 10 GB llenaría la memoria del gateway, y una bomba de descompresión pesa poco pero ocupa muchísimo al abrirse. Por eso C01 acepta fotos de hasta 10 MB.

El tope se revisa antes de leer el body. Content-Length llega en los headers, así que el gateway puede responder 413 («demasiado grande») sin haber recibido la foto: le cuesta casi nada. Cada pieza del camino tiene su propio tope, y el más chico manda:

  • El gateway de C01: 10 MB por foto.
  • El ALB de C01: no le pone tope al body. Sí a los headers: 64 KB entre todos; si se pasan, responde 400.
  • nginx, un servidor web y proxy muy usado: 1 MB por omisión (su opción client_max_body_size). Si un nginx sin configurar queda en el camino, una foto web de 3 MB recibe un 413 de él, no del gateway.
  • Amazon API Gateway, un servicio de AWS: un tope duro de 10 MB por request, que no se puede subir. Fue una de las razones por las que la Ronda no lo usó en C01 (M15).
  • Un endpoint en tiempo real de SageMaker AI, la plataforma de ML de AWS: 6 MiB (6,291,456 bytes) por request, y 60 segundos para responder (M13).

Las cifras de AWS son las de su documentación en 2026; los servicios en la nube cambian sus topes de vez en cuando, así que se revisan antes de diseñar. Y fíjate en lo que dice el caso de nginx: el 413 lo responde la pieza que tiene el tope. Si una pieza de en medio tiene uno más chico, el gateway nunca ve la request, y buscar ese 413 en sus logs no sirve de nada.

Prueba con la calculadora cuánto pesa la foto en cada forma y cuánto tarda en subir.

Cuánto pesa la foto en el bodyCalculadora

Bytes del body = bytes de la foto × (1 + 1/3 si va en base64). Arranca con la foto del celular de Ana, en bytes crudos.

Bytes del body200 KB
Tiempo de subidasolo los bytes; sin contar los saludos ni el RTT160 ms
Tráfico total, en Gb/slo que entra al gateway entre todas las fotos0.80
Parte del tope de 10 MB de C01más de 100 %: el gateway responde 4132.0 %
Parte del tope de 6 MiB de SageMaker AI6,291,456 bytes por request3.2 %

Base64 siempre suma un tercio. Con 9,000 KB, en crudo usas el 90 % del tope; en base64, el 120 %: 413.

Arranca con la foto del celular: 200 KB en bytes crudos, una subida de 10 Mb/s y el pico de 500 fotos por segundo. Pon 1 en base64 y mira crecer todo un tercio. Luego pon 9,000 KB: en base64 pasa de 10 MB y se vuelve un 413.

05

Códigos de estado: qué pasó y de quién es la culpa

Esencial

Status codes: what happened and whose fault it is

El código de estado es el número de tres cifras de la primera línea de la respuesta. La primera cifra dice la familia, y con ella ya sabes casi todo:

  • 1xx, informativo: «sigue». Casi no los vas a ver. El 101 aparece cuando una conexión se cambia a WebSocket, una conexión que queda abierta para mandar mensajes en los dos sentidos.
  • 2xx, éxito: salió bien.
  • 3xx, redirección: lo que buscas está en otro lado; el header Location dice dónde.
  • 4xx, error del cliente: tu request está mal. Si la repites igual, va a fallar igual.
  • 5xx, error del servidor: yo fallé. Quizá más tarde sí se pueda.
Las cinco familias de códigos de estado de HTTP. 1xx, informativo: sigue; por ejemplo, 101 al cambiar a WebSocket. 2xx, éxito: 200 y 202. 3xx, ve a otro lado: 301 y 308, el sitio se mudó, con el header Location que dice a dónde. 4xx, tu request está mal: 400, 401, 403, 404, 405, 413, 415, 422 y 429. Es culpa de quien pide y no se reintenta, salvo el 429 después de Retry-After. 5xx, yo fallé: 500, 502, 503 y 504. Es culpa de quien responde y se puede reintentar con cuidado, el 503 después de Retry-After. La línea entre el 4xx y el 5xx es la que más importa: separa «tu request está mal» de «yo fallé». LA PRIMERA CIFRA DICE LA FAMILIA 1xx Informativo: sigue 101: la conexión pasa a WebSocket 2xx Éxito 200 salió bien · 202 lo recibí 3xx Ve a otro lado 301 · 308 se mudó: mira Location 4xx Tu request está mal 400 · 401 · 403 · 404 · 405 413 · 415 · 422 · 429 Culpa de quien pide. No reintentes, salvo el 429, tras Retry-After. la línea que más importa 5xx Yo fallé 500 · 502 · 503 · 504 Culpa de quien responde. Reintenta con cuidado; el 503, tras Retry-After.
Las cinco familias de códigos, del 100 al 599. La primera cifra dice de quién es la culpa: la línea entre el 4xx y el 5xx separa «tu request está mal» de «yo fallé», y de ella depende si tiene sentido reintentar.

Los códigos de la Ronda

CódigoQué dice¿De quién es la culpa?¿Reintentar?Dónde aparece
200 OKSalió bien; la respuesta viene en el bodyDe nadieNo hace faltaC01, con sus 5 categorías
202 AcceptedRecibí tu pedido; el resultado, despuésDe nadieNo: pregunta luego por el resultadoM02: un POST que devuelve un job_id, el número del trabajo
400 Bad RequestTu request no tiene sentidoDel clienteNoC01: bytes que no son una imagen
401 UnauthorizedNo sé quién eresDel cliente: falta o no vale su credencialNo, hasta tener una credencial válidaC10: sin llave válida
403 ForbiddenSé quién eres y no puedesDel clienteNoUn equipo que pide un modelo que no le toca
404 Not FoundEso no existe aquíDel clienteNo/v1/clasify, mal escrito
405 Method Not AllowedLa ruta existe; el método, noDel clienteNoC01: un GET a /v1/classify
413 Content Too LargeEl body pasa el topeDel clienteNoC01: una foto de más de 10 MB
415 Unsupported Media TypeNo sé leer ese tipoDel clienteNoC01: un PDF, o un GIF
422 Unprocessable ContentEntiendo la forma, pero el contenido no valeDel clienteNoC01: top_k=0
429 Too Many RequestsTe pasaste de tu cuotaDel clienteSí, después de Retry-AfterM15 y C10: cuota por cliente
500 Internal Server ErrorAlgo falló adentro y no sé decir quéDel servidor: casi siempre un bugCon cuidadoC01: el modelo devolvió logits NaN, «no es un número»
502 Bad GatewaySoy intermediario y quien está detrás me contestó malDel servidor de atrásSí, con cuidadoC10: vLLM, el servidor del modelo, se cayó
503 Service UnavailableAhora no puedo: estoy lleno o me falta algoDel servidorSí, después de Retry-AfterC01: Triton caído, o un pod que ya atiende 16 requests a la vez, su tope (MAX_INFLIGHT)
504 Gateway TimeoutSoy intermediario y quien está detrás no contestó a tiempoDel servidor de atrásCon cuidado: el tiempo ya se gastóC01: Triton no respondió en 150 ms

Tres pares se confunden mucho:

  • 401 y 403. Al 401 le pusieron «no autorizado», pero quiere decir «no sé quién eres» (sin credencial, o con una vencida). El 403 es «sé quién eres y no te toca».
  • 400 y 422. Los dos dicen «tu entrada está mal». FastAPI, el framework de Python con que está escrito el gateway de C01, responde 422 por su cuenta cuando Pydantic, la librería que revisa los tipos de cada parámetro, rechaza uno: top_k=0, si el contrato pide de 1 a 20. El 400 lo usa el gateway para lo que revisa él mismo, como una imagen que no se puede decodificar.
  • 429 y 503. Los dos llevan Retry-After y los dos invitan a reintentar, pero la culpa es opuesta. El 429 dice «tú te pasaste de tu cuota; yo estoy bien». El 503 dice «yo estoy saturado; no es culpa tuya». Si respondes 503 por una cuota, tu alarma de disponibilidad se dispara por culpa de un cliente que se pasó (M15).

Los dos con «Gateway» en el nombre, el 502 y el 504, los responde un intermediario cuando el de atrás falla: el gateway cuando Triton no contesta a tiempo, o el ALB cuando el gateway le cierra la conexión. Y hay códigos que no están en ningún estándar: el 499 es una convención de nginx para «el cliente se fue antes de la respuesta». Lo ves en la sección extra que sigue.

Quién responde cada código

El código no siempre lo pone el servidor final. Cada pieza del camino puede contestar por su cuenta, y saber quién lo hizo es la mitad de cualquier diagnóstico.

Quién responde cada código en el camino de C01 · Toca cada pieza para ver qué códigos responde ella misma. Los recorridos siguen cuatro requests distintas: una que sale bien y tres que se quedan en el camino.
  • Backend · el cliente · espera 300 ms
    El programa del marketplace que recibe la publicación de Ana y pide la categoría. Es el cliente de esta conversación. Lee el código de cada respuesta para decidir: con un 4xx no reintenta; con un 503, un 504 o nada en 300 ms, muestra el selector manual, su fallback.
  • ALB · TLS · token · reparte
    El balanceador de carga de AWS. Termina HTTPS, verifica el token de Authorization y reparte las requests entre los 5 pods del gateway. Si el token no vale, responde 401 sin molestar al gateway.
  • Gateway · FastAPI · 5 pods
    Revisa la request de lo barato a lo caro: el método, el tamaño por Content-Length, el tipo, los parámetros y la foto. Después la prepara y llama a Triton con un timeout de 150 ms. Si todo sale bien, responde 200 con un JSON.
  • Triton · el modelo, en una GPU
    El servidor del modelo. Recibe el tensor por HTTP en el puerto 8000 y devuelve 500 números, uno por categoría. No habla con el backend: solo con el gateway.
  • 401 · 502 · token malo · gateway cerró
    Los responde el ALB. 401 si el token falta o venció: culpa del cliente. 502 si el gateway le cierra la conexión o le contesta algo que no es HTTP: culpa de atrás. Y si no tiene ningún pod registrado al que mandarla, un 503. (Si los tiene pero todos fallan sus revisiones de salud, el ALB les manda la request de todos modos.)
  • 405 · 413 · 415 · 422 · y 400 si no es imagen
    Los responde el gateway sin llegar al modelo, todos culpa del cliente: 405 con GET, 413 con más de 10 MB, 415 con un tipo que no es JPEG, PNG ni WebP, 422 con top_k fuera de 1 a 20 (lo pone FastAPI) y 400 si los bytes no se pueden decodificar. Ninguno se reintenta.
  • 503 · 504 · los pone el gateway por Triton
    Los responde el gateway por Triton. 503 con Retry-After si Triton está caído o si el pod ya atiende 16 requests a la vez, su tope (MAX_INFLIGHT). 504 si Triton no contesta en 150 ms. Culpa del servidor: el backend cae al selector manual.

Para qué sirve saber de quién es la culpa

Quien llama decide con el código. El backend de C01 hace así: con un 4xx no reintenta, porque fallaría igual, y lo anota como un error suyo; con un 503 o un 504, o si no llega nada en 300 ms, muestra el selector de categorías de siempre y Ana elige a mano. Ese plan B es el fallback de C01.

Y la culpa decide qué cuenta contra la promesa. En «Promedios, medianas y percentiles» viste el SLI de disponibilidad de C01: de las requests que cuentan, la fracción que salió sin error del servidor y en menos de 300 ms; su promesa, el SLO, es 99.9 %. Las que cuentan son solo las válidas: las que no fallaron por culpa de quien llama. Una foto de 50 MB que recibe 413 no es una falla del servicio.

El SLI de C01 en un día, contando bien los 4xxLa cuenta
Requests del día
—
10,000,000
Con 4xx413, 415, 422, 429…: culpa de quien llama
—
150,000
Válidas
10,000,000 − 150,000
9,850,000
Malas4,000 con 5xx y 3,000 lentas, de más de 300 ms
4,000 + 3,000
7,000
SLI
(9,850,000 − 7,000) ÷ 9,850,000
≈ 99.93 %
Si los 4xx contaran como fallas
(10,000,000 − 157,000) ÷ 10,000,000
≈ 98.43 %
Resultado
Contado bien, el día queda arriba del 99.9 % del SLO. Contando los 4xx, parecería un desastre por errores que no son del servicio.

Los números del día son un supuesto para el ejemplo; la regla es la de la Ronda: los 4xx no se reintentan y no cuentan contra el SLO.

Los códigos, en tu computadora

Este servidor imita las revisiones del gateway de C01 con un tope de juguete de 1,000 bytes. Revisa en el mismo orden que el gateway: primero lo barato.

python
import threading, urllib.error, urllib.request
from http.server import BaseHTTPRequestHandler, HTTPServer

class Gateway(BaseHTTPRequestHandler):          # tope de juguete: 1,000 bytes (en C01, 10 MB)
    def do_GET(self): self.responder(405)      # /v1/classify solo acepta POST
    def do_POST(self):
        largo, top_k = int(self.headers["Content-Length"]), int(self.path.split("top_k=")[1])
        if largo > 1000: codigo = 413           # se decide con el header, sin leer el body
        elif self.headers["Content-Type"] != "image/jpeg": codigo = 415
        elif not 1 <= top_k <= 20: codigo = 422
        else: codigo = 200
        self.rfile.read(largo)                  # la demo lo lee igual, para que corra estable
        self.responder(codigo)
    def responder(self, codigo):
        self.send_response(codigo); self.send_header("Content-Length", "0"); self.end_headers()
    def log_message(self, *args): pass

servidor = HTTPServer(("127.0.0.1", 0), Gateway)
threading.Thread(target=servidor.serve_forever, daemon=True).start()
url = f"http://127.0.0.1:{servidor.server_port}/v1/classify?top_k="

def pedir(metodo, top_k, tipo, body):
    req = urllib.request.Request(url + top_k, body, {"Content-Type": tipo}, method=metodo)
    try:
        with urllib.request.urlopen(req, timeout=2) as r: return r.status
    except urllib.error.HTTPError as e: return e.code     # urllib lanza un error con 4xx y 5xx

foto = b"\xff\xd8" + b"x" * 498                             # 500 bytes que empiezan como un JPEG
for caso in [("POST", "5", "image/jpeg", foto), ("GET", "5", "image/jpeg", None),
             ("POST", "5", "image/jpeg", foto * 4), ("POST", "5", "application/pdf", b"%PDF"),
             ("POST", "0", "image/jpeg", foto)]:
    print(f"{caso[0]:<4} top_k={caso[1]}  {caso[2]:<15} {len(caso[3] or b''):>5} bytes → {pedir(*caso)}")
Salida
POST top_k=5  image/jpeg        500 bytes → 200
GET  top_k=5  image/jpeg          0 bytes → 405
POST top_k=5  image/jpeg       2000 bytes → 413
POST top_k=5  application/pdf     4 bytes → 415
POST top_k=0  image/jpeg        500 bytes → 422

Fíjate en timeout=2: el cliente espera como máximo 2 segundos. Un cliente sin timeout puede quedarse esperando para siempre, y en Python, la librería requests no trae uno si no se lo pides (M03).

06

Leer una request sin confiar en ella

Extra

Reading a request without trusting it

Quien manda la request puede equivocarse o mentir. El gateway de C01 la lee como quien revisa un paquete sospechoso: primero lo que cuesta poco, y sin creerle a la etiqueta.

No le creas a la etiqueta

Content-Type y Content-Length los escribe quien llama, y quien llama puede equivocarse. Por eso el gateway de C01 no se queda con la etiqueta: también cuentan los primeros bytes del body, que en cada formato de imagen son fijos. Un JPEG empieza con FF D8 FF; un PNG, con 89 50 4E 47 (los tres últimos son las letras «PNG»); un WebP, con las letras «RIFF» y, cuatro bytes después, «WEBP». Pillow, la librería con que el gateway abre las fotos, reconoce el formato por esos bytes, no por la etiqueta, y solo acepta los tres permitidos. Si la etiqueta dice image/jpeg pero los bytes dicen otra cosa, Pillow no la abre y la request recibe un 400. La etiqueta sirve para rechazar rápido (415); los bytes, para confirmar.

Cuando no hay Content-Length

A veces el cliente no sabe de antemano cuánto va a mandar: por ejemplo, si va leyendo el archivo mientras lo sube. Entonces usa Transfer-Encoding: chunked: manda el body en pedazos, cada uno precedido por su tamaño en hexadecimal, y un pedazo de tamaño 0 marca el final.

Así se ve un body de 20 bytes mandado en dos pedazos. El a es 10 en hexadecimal y el 0 final dice «ya no hay más»:

text
POST /v1/classify?top_k=5 HTTP/1.1
Host: classifier.internal
Content-Type: image/jpeg
Transfer-Encoding: chunked

a
(10 bytes de la foto)
a
(otros 10 bytes)
0

Sin Content-Length, el truco del 413 gratis no sirve. El gateway tiene que contar mientras lee y cortar en cuanto pasa del tope. Así lo hace el código de referencia de la Ronda: lee el body en pedazos, suma sus tamaños y responde 413 al pasar de 10 MB, sin haber guardado nada más. La misma cuenta protege de un cliente que miente en Content-Length y anuncia menos de lo que manda.

El cliente que se va

El backend le está pasando al gateway una foto de 100,000 bytes y, cuando lleva 1,000, la conexión se cierra: el backend se reinició, o se cansó de esperar. ¿Qué código registra el gateway? No hay a quién responderle. La convención de nginx es anotarlo como 499, «el cliente cerró la conexión», y el gateway de C01 la copia. No es un 5xx, porque no falló el servidor; se cuenta con los 4xx, fuera de la tasa de errores del servicio. Si se contara como 500, cada reinicio del backend le quitaría un poco al SLO. Cada pieza tiene sus propios números para esto: el ALB de AWS, por ejemplo, anota un 460 si el cliente se va mientras espera la respuesta, y un 400 si se va a media subida.

El cliente que va muy despacio

Otro abuso: abrir una conexión y mandar un byte por segundo. Cada conexión así ocupa memoria y un lugar en el servidor, sin costarle casi nada al atacante. Por eso los servidores ponen un tiempo máximo para recibir los headers y el body, y cierran lo que va demasiado lento. Los headers también tienen tope de tamaño: cada servidor pone el suyo, de unos KB a unas decenas de KB (nginx, 8 KB por header; el ALB, 64 KB entre todos). Si se pasa, el estándar tiene un código, el 431 (Request Header Fields Too Large), pero muchos servidores, nginx y el ALB entre ellos, responden un 400 a secas.

El orden de las revisiones

  1. 1

    Método y ruta

    ¿Existe la ruta? ¿Con ese método? Si no, 404 o 405. Cuesta microsegundos.

  2. 2

    Identidad

    ¿Trae una credencial válida? ¿Le toca esta ruta? Si no, 401 o 403. En C01 lo revisa el ALB, así que en la práctica pasa incluso antes del paso 1, cuando la request ni ha llegado al gateway.

  3. 3

    Cuota

    ¿Le queda cuota a este cliente? Si no, 429 con Retry-After.

  4. 4

    Headers del body

    ¿Content-Length pasa del tope? 413. ¿El tipo es JPEG, PNG o WebP? Si no, 415. ¿Los parámetros valen? Si no, 422.

  5. 5

    El body, contando

    Se lee en pedazos y se corta al pasar de 10 MB: 413. Si el cliente se va, 499.

  6. 6

    La foto

    Se revisan sus primeros bytes y sus megapíxeles, y se decodifica: 400 si no es una imagen. Aquí ya se gasta CPU de verdad.

  7. 7

    El modelo

    Si Triton no contesta, 503; si tarda más de su tiempo, 504. Aquí se gasta GPU.

Cada paso es más caro que el anterior. Rechazar en el paso 1 cuesta casi nada; rechazar en el 7 ya gastó CPU, memoria y GPU. Por eso lo barato va primero: es la misma idea de las guardas de la lección de imágenes.

07

HTTPS, keep-alive y pools: no pagues dos veces la conexión

Esencial

HTTPS, keep-alive and pools: don't pay for the connection twice

HTTPS: el mismo HTTP, en un sobre cerrado

El HTTP que viste viaja legible. Cualquiera en el camino (el wifi de una cafetería, un router de la ruta) puede leer el token de Authorization y la foto, y hasta cambiarlos. HTTPS es el mismo HTTP metido dentro de TLS, una capa que cifra todo lo que pasa por la conexión y comprueba que el servidor es quien dice ser, con un certificado: un documento digital que dice «este nombre es de este servidor», firmado por una organización en la que tu sistema ya confía. Cómo lo logra es tema de la lección de cifrado, en la fase 9.

Los mensajes de HTTP no cambian en nada: la misma primera línea, los mismos headers, el mismo body. Solo viajan cifrados. Por omisión, HTTPS usa el puerto 443. Por fuera se sigue viendo a qué IP y puerto vas, cuánto pesan los mensajes y, casi siempre, el nombre del host, porque el cliente lo manda sin cifrar al saludar, para que el servidor sepa qué certificado mostrar. Hay una extensión nueva que también lo esconde, pero todavía se usa poco. La ruta, los headers y el body quedan ocultos.

En C01, el ALB termina TLS: descifra la request y la manda al gateway por la red privada del sistema. La Ronda pide además TLS en todo lo que sale de un pod y, dentro del cluster (el grupo de máquinas donde corren los pods), mTLS si hay una malla de servicios (proxies junto a cada pod, en la lección de balanceadores): TLS en los dos sentidos, donde también el cliente muestra su certificado (M15).

Lo que cuesta abrir una conexión

Antes del primer byte de HTTP hay que abrir la conexión. Como viste en la lección anterior, TCP abre con un handshake, un saludo de tres mensajes que cuesta un RTT (round-trip time: lo que tarda un mensaje en ir y volver). Encima va el saludo de TLS: 1 RTT más con TLS 1.3, la versión actual (de 2018), y 2 con TLS 1.2, la anterior. Solo entonces viajan la request y la respuesta: otro RTT, más lo que tarde el servidor.

La primera request desde el celular de AnaLa cuenta
RTT del celular al servidorsupuesto: 4G, servidor en el mismo país
—
50 ms
Handshake de TCP
1 × 50 ms
50 ms
Handshake de TLS 1.3
1 × 50 ms
50 ms
Request y respuesta
1 × 50 ms
50 ms
El servidor trabajasupuesto
—
40 ms
Con una conexión nueva
50 + 50 + 50 + 40
190 ms
Con la conexión ya abierta
50 + 40
90 ms
Con una nueva y TLS 1.22 RTT de TLS
50 + 100 + 50 + 40
240 ms
Resultado
Abrir la conexión cuesta más que la request misma: 100 ms de saludos contra 90 de trabajo útil. Reusarla los ahorra todos.
La misma request con un RTT de 50 ms y 40 ms de trabajo del servidor, en dos líneas de tiempo que bajan. A la izquierda, con una conexión nueva: primero el saludo de TCP, una ida y vuelta de 50 ms; después el saludo de TLS 1.3, otra ida y vuelta; después la request, los 40 ms del servidor y la respuesta, que llega a los 190 ms. A la derecha, con una conexión que ya estaba abierta: solo la request, los 40 ms del servidor y la respuesta, que llega a los 90 ms. Se ahorran 100 ms, los dos saludos. RTT DE 50 MS · EL TIEMPO CORRE HACIA ABAJO CONEXIÓN NUEVA YA ABIERTA cliente servidor cliente servidor 40 ms TCP 1 RTT TLS 1 RTT HTTP 1 RTT 40 ms 90 ms llega la respuesta 100 ms ahorrados: los saludos de TCP y TLS ya se pagaron 190 ms llega la respuesta TCP TLS 1.3 la request y su respuesta
La misma request, con una conexión nueva y con una que ya estaba abierta. A la izquierda, tres viajes de ida y vuelta antes de tener la respuesta; a la derecha, uno solo.

Keep-alive: dejar la conexión abierta

Keep-alive es no cerrar la conexión después de la respuesta, para mandar por ella la siguiente request. En HTTP/1.1 es lo normal: la conexión queda abierta salvo que alguien mande Connection: close. El servidor la cierra cuando pasa un rato sin uso, su idle timeout (tiempo máximo de inactividad).

Dentro de un centro de datos el RTT es de una fracción de milisegundo, y los saludos cuestan poco en tiempo. Pero cada conexión nueva también cuesta CPU, sobre todo con TLS, y deja rastro en el sistema operativo. El gateway de C01 crea un solo cliente de Triton por proceso y lo reusa en todas las requests: reusa sus conexiones y no hace un handshake por foto.

El pool de conexiones

En HTTP/1.1, una conexión lleva una request a la vez: la segunda espera a que llegue la respuesta de la primera. Si el gateway quiere 16 llamadas a Triton al mismo tiempo, necesita 16 conexiones.

Un pool de conexiones es un grupo de conexiones abiertas que el cliente guarda y presta. Cada request toma una libre, la usa y la devuelve. Si no hay libre y el pool no ha llegado a su tamaño máximo, abre otra; si ya llegó, la request espera su turno. Así el pool también es un tope: limita cuántas llamadas van a la vez hacia un servidor.

En Python, urllib.request.urlopen abre y cierra una conexión cada vez. http.client.HTTPConnection la deja abierta. El servidor de abajo cuenta cuántas conexiones recibe:

python
import http.client, threading, urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

class Gateway(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"               # permite dejar la conexión abierta
    conexiones = 0
    def setup(self):                            # corre una vez por cada conexión nueva
        Gateway.conexiones += 1
        super().setup()
    def do_GET(self):
        self.send_response(200); self.send_header("Content-Length", "2"); self.end_headers()
        self.wfile.write(b"ok")
    def log_message(self, *args): pass

servidor = ThreadingHTTPServer(("127.0.0.1", 0), Gateway)   # atiende cada conexión en su hilo
threading.Thread(target=servidor.serve_forever, daemon=True).start()
host, puerto = servidor.server_address

for _ in range(10):                             # urllib abre y cierra una conexión cada vez
    urllib.request.urlopen(f"http://{host}:{puerto}/v2/health/ready").read()
print("10 requests con urllib:          ", Gateway.conexiones, "conexiones")

Gateway.conexiones = 0
con = http.client.HTTPConnection(host, puerto)  # una conexión que se reusa (keep-alive)
for _ in range(10):
    con.request("GET", "/v2/health/ready"); con.getresponse().read()
print("10 requests con una HTTPConnection:", Gateway.conexiones, "conexión")
Salida
10 requests con urllib:           10 conexiones
10 requests con una HTTPConnection: 1 conexión

Fuera de la biblioteca estándar, lo mismo hacen requests.Session y httpx.Client: crea uno y reúsalo, en vez de un cliente nuevo por request. Con la calculadora, juega con el RTT y con cuántas requests van por cada conexión.

Lo que cuesta abrir una conexiónCalculadora

Primera request = (1 de TCP + los de TLS + 1) × RTT + servidor. Las siguientes, por la misma conexión, solo pagan 1 × RTT + servidor. Arranca con el celular de Ana.

Primera request, con conexión nueva190.0 ms
Las siguientes, por la conexión abierta90.0 ms
Promedio por requestlos saludos se reparten entre las requests de la conexión100.0 ms
Parte del tiempo que se va en saludos10.0 %
Ahorro por request, contra abrir una conexión cada vez90.0 ms

Con 1 request por conexión, el ahorro es 0 y más de la mitad del tiempo son saludos. Con 0.5 ms de RTT, los saludos casi no cuestan tiempo, pero cada conexión nueva sigue costando CPU y puertos.

Arranca con el celular de la cuenta: RTT de 50 ms, TLS 1.3, 40 ms del servidor y 10 requests por conexión. Pon 1 request por conexión para ver el caso sin keep-alive. Luego baja el RTT a 0.5 ms, como dentro de un centro de datos: los saludos casi desaparecen.

Los dos lados tienen que estar de acuerdo

Una conexión abierta tiene dos dueños, y cada uno tiene su idle timeout. El ALB cierra las que pasan 60 segundos sin uso; uvicorn, el servidor que corre el gateway, a los 5 si nadie le dice otra cosa (los dos son sus valores por omisión en 2026). Si el de atrás cierra primero, el ALB a veces manda una request por una conexión que acaba de morir, y responde 502. Por eso la Ronda corre uvicorn con --timeout-keep-alive 75: así siempre cierra primero el ALB. Lo desarmas en el último reto.

Para ir más hondoLos puertos que deja cada conexión cerrada

Lo viste en la lección anterior: el lado que cierra primero una conexión TCP deja su puerto de origen apartado 60 segundos (el estado TIME_WAIT), y Linux solo tiene 28,232 puertos de origen. Ahora ponle HTTP encima.

Un cliente que abre una conexión por request hacia un mismo servidor, y la cierra él, al pico de C01: 500 conexiones nuevas por segundo × 60 s = 30,000 puertos apartados. No caben en 28,232, y las conexiones nuevas fallan con un error que no parece de HTTP: «no se puede asignar la dirección pedida». El límite es por destino: si las mismas 500 se reparten entre los 5 pods del gateway, cada uno con su IP, son 100 por segundo × 60 s = 6,000 por pod, y caben. Pero una sola máquina de pruebas de carga que manda 1,000 por segundo a un solo destino sin keep-alive aparta 60,000 y se queda sin puertos. Con keep-alive se abren unas cuantas conexiones y el problema desaparece.

08

HTTP/2 y HTTP/3

Extra

HTTP/2 and HTTP/3

HTTP/1.1 tiene dos límites que ya viste: una request a la vez por conexión, y headers en texto que se repiten enteros en cada request. HTTP/2, de 2015, los resuelve sin cambiar el significado: los mismos métodos, los mismos headers, los mismos códigos. Cambia el cableado.

  • Binario. Ya no son líneas de texto, sino frames: pedacitos con un encabezado de tamaño fijo que dice qué son y a qué request pertenecen. La primera línea desaparece: el método, la ruta y el código viajan como headers especiales (:method, :path, :status), y los nombres de los headers van en minúsculas.
  • Varias requests a la vez en una conexión. Cada request es un stream con su número, y los frames de distintos streams se intercalan. Es la multiplexación: decenas de requests en vuelo por una sola conexión, y una respuesta lenta no detiene a las demás.
  • Headers comprimidos. Los dos lados recuerdan los headers ya enviados (la compresión se llama HPACK). Un header que se repite igual en cada request, como User-Agent o Content-Type, viaja como un número de uno o dos bytes. (Los secretos, como Authorization, se pueden marcar para que nunca entren a esa memoria: compartirla abre la puerta a adivinarlos.)

Una sola conexión para todo: un solo saludo. gRPC, un sistema creado por Google para que un programa llame funciones de otro por la red, que verás en la lección siguiente, viaja sobre HTTP/2. Triton lo atiende en su puerto 8001; HTTP/1.1 en el 8000.

gRPC usa HTTP/2 a su manera. La respuesta HTTP casi siempre es un 200, aunque la llamada falle: el resultado real viaja en un header que llega al final del stream (un trailer), grpc-status, con códigos propios de gRPC, como UNAVAILABLE (el parecido al 503) o DEADLINE_EXCEEDED (el parecido al 504). Y el tiempo máximo de la llamada viaja en otro header, grpc-timeout, para que el servidor sepa cuánto le queda a quien espera (M03). Si mides errores mirando solo el código HTTP, un servicio de gRPC siempre se ve sano.

La trampa: se balancean conexiones, no requests

Un Service de Kubernetes, el nombre fijo con el que los pods del gateway encuentran a los de Triton, reparte conexiones nuevas entre los pods de atrás; no mira las requests que van adentro. Con HTTP/2, cada gateway abre una conexión, le toca un Triton y le manda todo por ahí durante horas.

Se reparten conexiones, no requests · Los mismos gateways y los mismos Triton, con un pool de HTTP/1.1 y con una conexión de HTTP/2. Recorre el tercer camino: el Triton que acaba de llegar no recibe nada.
  • Gateway A · un pod
    Un pod del gateway. Con HTTP/1.1 tiene un pool de varias conexiones hacia Triton; con HTTP/2 o gRPC, normalmente una sola, larga, por la que manda todas sus requests.
  • Gateway B · otro pod
    Otro pod del gateway, igual al primero. Cada uno elige a qué Triton conectarse solo en el momento de abrir cada conexión.
  • Service · el nombre fijo de Triton
    Un Service de Kubernetes. No es un programa por el que pasen las requests: son reglas en cada máquina que eligen un pod de Triton cuando se abre una conexión. Lo que viaje después por esa conexión ya no lo mira.
  • Triton 1 · ya estaba
    Un pod de Triton que ya estaba corriendo. Tiene las conexiones que se abrieron cuando llegó.
  • Triton 2 · ya estaba
    Otro pod de Triton que ya estaba. Con HTTP/2, cada gateway quedó pegado a uno de estos dos.
  • Triton 3 · recién llegado
    El pod que agregó el autoescalado porque subió el tráfico. Solo recibe trabajo cuando alguien abre una conexión nueva y le toca a él.

La Ronda (M06 y M07) cuenta la consecuencia: llega un Triton nuevo porque subió el tráfico y se queda casi sin trabajo, mientras los viejos siguen saturados. Con HTTP/1.1 y keep-alive el efecto existe, pero es menor, porque el pool abre varias conexiones y las va renovando. Las salidas: un intermediario que reparta por request, como un service mesh (una red de proxies, uno junto a cada pod); un tiempo de vida máximo para cada conexión, que obliga a reconectar; o que el cliente conozca todos los pods y reparta él mismo. La lección de balanceadores las abre.

HTTP/2 todavía depende de TCP

En la lección anterior viste el bloqueo por cabeza de fila (head-of-line blocking): TCP entrega en orden, así que si se pierde un paquete, todo lo que venía detrás espera su reenvío. Con HTTP/2 duele más, porque lo que espera detrás es de otros streams: con 30 streams en una conexión, un paquete perdido frena a los 30. En una red con pérdidas, como un celular en movimiento, se nota.

HTTP/3, estandarizado en 2022, es HTTP sobre QUIC, el protocolo que también viste allá: va sobre UDP (paquetes sueltos, sin orden ni reenvíos) y hace él mismo el trabajo de TCP, pero por stream, así que un paquete perdido solo frena a su stream. Junta el saludo de la conexión y el de TLS 1.3 en un solo RTT, y al reconectarse con un servidor conocido puede mandar la request desde el primer mensaje. Lo usan sobre todo los navegadores y las CDN, las redes de servidores cerca de los usuarios. Entre servicios de un mismo centro de datos, donde casi no se pierden paquetes, siguen mandando HTTP/1.1 y HTTP/2.

VersiónAñoFormatoRequests por conexiónVa sobre
HTTP/1.11997TextoUna a la vezTCP
HTTP/22015Binario, en framesMuchas, intercaladasTCP
HTTP/32022Binario, en framesMuchas, independientesQUIC, sobre UDP
09

Errores comunes

Esencial

Common mistakes

1. Reintentar un 4xx

Un 400, un 413 o un 415 van a fallar igual la segunda vez: la foto sigue siendo un PDF. Reintentarlos solo agrega carga.

Cómo evitarlo: reintentar solo lo que puede cambiar: 503 y 429 (respetando Retry-After) y, con cuidado, 502 y 504. Nunca un 400, 413 o 415 (M03).

2. Un código que miente

Responder 503 cuando el cliente se pasó de su cuota esconde de quién es la culpa y dispara tu alarma de disponibilidad. Peor aún es un 200 que miente: un plan B que responde 200 sin que nadie lo cuente, o un servidor de modelo roto que devuelve basura con código 200. Los tableros se ven verdes y el modelo no está contestando (M02, M11).

Cómo evitarlo: 429 para la cuota, 503 para la saturación. Si respondes con un plan B, márcalo (un header o un campo del body) y cuéntalo en una métrica aparte.

3. Un cliente sin timeout

Si el servidor se cuelga, un cliente sin timeout se cuelga con él, con su conexión y su memoria. requests de Python no trae timeout por omisión (M03).

Cómo evitarlo: pon siempre un timeout, y que sea menor que el de quien te llama: en C01, el backend espera 300 ms, el gateway se da 250 y a Triton le da 150.

4. Leer el body sin tope

Un gateway que lee el body completo antes de revisarlo puede llenarse de memoria con una sola request de varios GB.

Cómo evitarlo: revisar Content-Length antes de leer (413 casi gratis) y, además, contar mientras se lee, porque un cliente puede mandar el body en pedazos o mentir en el header.

5. Un GET que cambia cosas

GET /v1/borrar?id=7 es una mala idea: los navegadores, los proxies y los reintentos automáticos asumen que un GET no tiene efectos y lo repiten sin preguntar. Al revés, un POST que crea algo y se reintenta por un timeout puede crearlo dos veces.

Cómo evitarlo: GET solo para leer. Para un POST que se reintenta, una llave de idempotencia: un header (Idempotency-Key) con el que el servidor reconoce una request repetida. Lo ves en «Timeouts, reintentos e idempotencia».

6. El de atrás cierra antes que el de adelante

Si el gateway cierra sus conexiones inactivas a los 5 segundos y el ALB las sigue usando hasta los 60, aparecen 502 sueltos que nadie entiende.

Cómo evitarlo: el idle timeout de cada pieza, mayor que el de la pieza que tiene enfrente: uvicorn con --timeout-keep-alive 75 detrás de un ALB de 60 s.

10

En la vida real

Extra

In real life

C01: el contrato completo

El gateway de C01 tiene una sola ruta de trabajo, POST /v1/classify?top_k=5, que recibe los bytes de la foto y devuelve el top-k de categorías con su score y la model_version. Acepta JPEG, PNG y WebP de hasta 10 MB y 40 MP. Sus errores están escritos en los requisitos: 400 si no es una imagen, 413 si pesa demasiado, 415 si el tipo no está permitido, 503 con Retry-After si Triton no está disponible y 504 si se vence el tiempo de la inferencia. FastAPI pone el 422.

Cada código tiene un camino claro del otro lado. Los 4xx no se reintentan ni cuentan contra el SLO. Con un 503, un 504 o nada en 300 ms, el backend muestra el selector manual. Y la model_version de cada respuesta se guarda con la publicación: así se puede preguntar después qué modelo sugirió cada categoría.

Detrás, el gateway habla HTTP con Triton en el puerto 8000, con el protocolo de inferencia KServe v2 (una forma estándar de pedirle predicciones a un servidor de modelos, que verás en la lección siguiente). Triton también responde a GET /v2/health/ready: 200 si ya puede atender, para que Kubernetes le mande tráfico solo cuando está listo.

C10: un LLM detrás de una API

C10 sirve un LLM a varios equipos de una empresa, y su gateway habla con los códigos de esta lección. Sin llave válida, 401. Un prompt que pasa el tope del equipo, 400, con un mensaje claro y nunca recortado en silencio. Sin cuota de tokens, 429 con Retry-After. Con las GPUs llenas, 503 con Retry-After: mejor eso que hacer esperar 8 segundos por la primera palabra. Si vLLM, el servidor del modelo, se cae o se traba, 502 o 504.

La respuesta llega en pedazos, con Content-Type: text/event-stream: un data: por cada fragmento de texto y un data: [DONE] al final. Sigue siendo HTTP; solo que el body se escribe poco a poco. Y su alerta de errores cuenta los 5xx y los streams cortados, pero no los 429 por cuota ni los 499 de quien se fue: no son fallas de la plataforma.

Los servidores de modelos administrados

SageMaker AI, la plataforma de ML de AWS, te pide un contenedor que escuche en el puerto 8080 y atienda dos rutas: POST /invocations para la inferencia y GET /ping para la salud. Cada request puede traer hasta 6 MiB, y el contenedor tiene 60 s para responder (los topes de su documentación en 2026). Las fotos web de C01, de hasta 10 MB, no entran tal cual: o el gateway las reduce antes, o el patrón cambia a uno asíncrono, con la foto en S3, el almacén de archivos de AWS, y un 202 que dice «ya la recibí» (M13).

C02: cámaras que mandan eventos

En C02, la computadora pequeña de cada centro de distribución, conectada a las cámaras (la «caja»), detecta un evento en una cámara y se lo manda a una API de la nube por HTTPS. La API revisa el token del sitio, lo anota y responde 202 sin esperar a que nadie lo verifique: «lo recibí». Si la caja no recibe respuesta y reintenta, el evento no se duplica, porque la API lo registra con su event_id y reconoce el repetido.

Las fotos y el clip del evento, en cambio, no pasan por esa API. Antes de mandar el evento, la caja le pide a la API, con un POST chiquito que responde 200, una URL firmada de S3 para cada archivo: una URL que trae escrito en su query un permiso para subir ese archivo, y nada más, durante 15 minutos. Después sube el archivo con un PUT directo a esa URL. Como el permiso ya viaja en la URL, esa request no lleva header Authorization. Al final manda el evento, con los nombres de los archivos ya subidos. Así los megabytes de video no cruzan la API ni su balanceador.

Fuera de ML

  • Tu navegador. Abrir una página de noticias son decenas de requests: el HTML, las imágenes, los estilos, los scripts. Con HTTP/2, casi todas van por una sola conexión.
  • El 404 que conoces. Es este mismo código, con una página bonita en el body.
  • Un sitio que se mudó. Si escribes http:// y el sitio solo atiende https://, te contesta un 301 (algunos usan el 308, que es lo mismo pero obliga a repetir el mismo método) con Location: https://…, y el navegador te lleva solo.
  • curl -v. El comando curl hace requests desde la terminal, y con -v imprime los headers que manda (con >) y los que recibe (con <). Es la forma más rápida de ver esta lección en vivo con cualquier API.
11

Ejercicios

Esencial

Exercises

Resuélvelos con lápiz antes de abrir la solución. Los de body y conexiones se pueden comprobar con las calculadoras.

  1. 1Fácil

    Desarma esta URL: https://api.tienda.mx/v2/buscar?q=tenis%20rojos&page=2. ¿Qué viaja en la primera línea de la request y qué en el header Host? ¿A qué puerto se conecta?

    Ver la solución paso a paso
    • Esquema: https. Host: api.tienda.mx. Ruta: /v2/buscar. Query: q=tenis%20rojos&page=2, es decir, q = «tenis rojos» y page = 2.
    • No trae puerto, así que se usa el de omisión de HTTPS: 443.
    • Primera línea: GET /v2/buscar?q=tenis%20rojos&page=2 HTTP/1.1. Header: Host: api.tienda.mx. El esquema y el puerto no se escriben: ya se usaron para abrir la conexión.
  2. 2Fácil

    Para cada acción, elige método y di si es seguro e idempotente: (a) ver el estado de un trabajo, (b) subir una foto para clasificarla, (c) reemplazar la configuración completa de un modelo, (d) cambiar solo el precio de una publicación, (e) borrar un trabajo.

    Ver la solución paso a paso
    • (a) GET: seguro e idempotente.
    • (b) POST: ni seguro ni idempotente por definición del método, aunque clasificar no cambie nada en el servidor.
    • (c) PUT: no es seguro, pero sí idempotente: mandar la misma configuración dos veces deja lo mismo.
    • (d) PATCH: no es seguro y no se garantiza que sea idempotente.
    • (e) DELETE: no es seguro, pero sí idempotente: después de uno o de dos, el trabajo está borrado.
  3. 3Fácil

    ¿Qué código responde el sistema de C01, y de quién es la culpa? (a) Una foto de 14 MB. (b) Un GIF. (c) top_k=50. (d) Triton está caído. (e) Triton tarda 400 ms. (f) Un token vencido.

    Ver la solución paso a paso
    • (a) 413, del cliente: pasa de 10 MB.
    • (b) 415, del cliente: solo JPEG, PNG y WebP.
    • (c) 422, del cliente: FastAPI rechaza el parámetro fuera de 1 a 20.
    • (d) 503 con Retry-After, del servidor.
    • (e) 504, del servidor: el gateway corta a los 150 ms.
    • (f) 401, del cliente: lo responde el ALB, que verifica el token antes de que la request llegue al gateway.
    • Los tres primeros y el último no se reintentan; los del medio sí, y el backend cae al selector manual.
  4. 4Fácil

    Una foto de 150,000 bytes va en base64 dentro de un JSON. ¿Cuántos bytes ocupa? ¿Y una de 9 MB, con el tope de 10 MB de C01?

    Ver la solución paso a paso
    • 150,000 ÷ 3 = 50,000 grupos; × 4 = 200,000 bytes, un tercio más.
    • 9,000,000 ÷ 3 × 4 = 12,000,000 bytes: 12 MB. Pasa el tope: 413. En bytes crudos, 9 MB sí entraban.
    • La foto más grande que cabe en 10 MB como base64: 10,000,000 × 3 ÷ 4 = 7.5 MB.
  5. 5Medio

    Escribe la request completa (primera línea, headers y body) para preguntar por el trabajo 7f3c con el token abc123, y una respuesta de «todavía no termina». ¿Qué cambia si el trabajo no existe?

    Ver la solución paso a paso
    text
    GET /v1/jobs/7f3c HTTP/1.1
    Host: ocr.internal
    Authorization: Bearer abc123
    Accept: application/json
    
    • Sin body: un GET no lleva. Después de los headers va la línea en blanco, y ahí termina.
    • La respuesta: HTTP/1.1 200 OK, Content-Type: application/json, su Content-Length, la línea en blanco y un body como {"status": "running"}. La request salió bien aunque el trabajo no haya terminado: el estado va en el body.
    • Si el trabajo no existe: 404 Not Found. Si existe pero es de otro cliente, también conviene un 404 y no un 403, para no confirmarle a nadie que ese trabajo existe.
  6. 6Medio

    Un cliente hace 20 requests seguidas a un servidor con un RTT de 80 ms. Sin contar el tiempo del servidor, ¿cuánto tarda en total sin keep-alive y con keep-alive, con TLS 1.3 y con TLS 1.2?

    Ver la solución paso a paso
    • Sin keep-alive, cada request paga todo. Con TLS 1.3: (1 + 1 + 1) × 80 = 240 ms; × 20 = 4,800 ms. Con TLS 1.2: (1 + 2 + 1) × 80 = 320 ms; × 20 = 6,400 ms.
    • Con keep-alive, solo la primera paga los saludos. TLS 1.3: 240 + 19 × 80 = 1,760 ms. TLS 1.2: 320 + 19 × 80 = 1,840 ms.
    • Se ahorra ≈ 63 % con TLS 1.3 y ≈ 71 % con TLS 1.2. Con keep-alive, la versión de TLS casi deja de importar.
  7. 7Medio

    En un día, el gateway de C01 recibe 8,000,000 requests: 120,000 con 4xx, 9,000 con 5xx y 2,000 lentas (de más de 300 ms, sin error). ¿Cuál es el SLI? ¿Cumple el SLO de 99.9 %? ¿Y si alguien contara los 4xx como fallas?

    Ver la solución paso a paso
    • Válidas: 8,000,000 − 120,000 = 7,880,000.
    • Malas: 9,000 + 2,000 = 11,000.
    • SLI: (7,880,000 − 11,000) ÷ 7,880,000 ≈ 99.86 %. No cumple: el presupuesto del día era 0.1 % de 7,880,000 = 7,880 requests, y se gastaron 11,000.
    • Con los 4xx como fallas: (8,000,000 − 131,000) ÷ 8,000,000 ≈ 98.36 %. Se vería peor de lo que es, y se perdería la pista de que el problema son los 5xx.
  8. 8Reto

    Un equipo quiere llamar directo a un endpoint de SageMaker AI, que acepta hasta 6 MiB por request, con las fotos web de C01 en JSON con base64. (a) ¿Cuál es la foto más grande que cabe? (b) ¿Y en bytes crudos? (c) ¿Qué propones para las fotos de hasta 10 MB?

    Ver la solución paso a paso
    • (a) 6 MiB = 6,291,456 bytes. En base64 caben 6,291,456 × 3 ÷ 4 = 4,718,592 bytes, ≈ 4.7 MB, y un poco menos por el resto del JSON.
    • (b) En bytes crudos, casi los 6,291,456: ≈ 6.3 MB. Cualquier foto entre 6.3 y 10 MB no entra de ninguna forma.
    • (c) Dos salidas, las de la Ronda (M13): que el gateway reduzca la foto antes de llamar (a 1024 px, como el celular, pesa ≈ 200 KB), o cambiar al patrón asíncrono: la foto se sube a S3, la request solo lleva su dirección y la respuesta es un 202 con un job_id.
  9. 9Reto

    Los 502 aparecen de a poquito: 1 o 2 por cada 100,000 requests, sin relación con el tráfico. Los logs del gateway no muestran ningún error. El ALB cierra conexiones inactivas a los 60 s y uvicorn corre con su valor por omisión, 5 s. Explica qué pasa, por qué el gateway no lo ve y cómo lo arreglas.

    Ver la solución paso a paso
    • El ALB guarda conexiones abiertas hacia cada pod del gateway. Uvicorn cierra las que llevan 5 s sin uso; el ALB las seguiría usando hasta los 60 s.
    • A veces el ALB elige una conexión justo cuando uvicorn la está cerrando. La request llega a una conexión muerta; el ALB no recibe respuesta y contesta 502.
    • El gateway no lo ve porque, para él, esa request nunca llegó: no hay línea de log ni métrica. Solo el ALB la cuenta. Por eso es raro y no depende del tráfico: depende de cuándo coincide una request con un cierre.
    • Arreglo: que el de atrás espere más que el de adelante. --timeout-keep-alive 75 en uvicorn, mayor que los 60 s del ALB: así siempre cierra primero el ALB, que sabe que cerró y no reusa esa conexión.
12

Lo que sigue

Esencial

What comes next

En la Ronda:

  • M02 · Patrones de inferencia usa los códigos como contrato: la API síncrona de C01 con 400, 413, 415, 503 con Retry-After y 504; la asíncrona con POST → 202 + job_id y un GET para preguntar; y el 429 de una cuota.
  • M07 · Servidores de modelos abre los protocolos: Triton con HTTP en el 8000 y gRPC en el 8001, la extensión binaria de HTTP contra JSON y base64, HTTP/1.1 con un pool contra una conexión larga de HTTP/2, y los idle timeouts de los proxies.
  • M15 · Seguridad y confiabilidad separa el 429 del 503 y pone el Retry-After en segundos; el header Authorization con un JWT verificado en el ALB, y HTTPS en todo lo que sale de un pod.
  • También: M03 decide qué códigos se reintentan, M11 vigila la tasa de 5xx de un canary (la versión nueva, probada primero con una parte chica del tráfico) y M13 cuenta los topes de body de cada plataforma.

En el Dojo: la estación 6, «Código de serving», escribe el gateway de C01: la ruta POST /v1/classify, las revisiones en orden y los códigos de esta lección. La estación 11, «La request en vivo», sigue una request de punta a punta. Cuando abran, ahí lo vas a practicar.

En Sensei: «APIs: REST, JSON, gRPC y streaming» diseña la API que va encima de HTTP. «Balanceadores, proxies y gateways» abre las piezas que responden 502, 503 y 429 por su cuenta. «Timeouts, reintentos e idempotencia» decide qué hacer con cada código. Y «Qué tan rápido y cuánto: latencia y throughput» mide lo que cuesta cada viaje.

13

Palabras que vas a oír

Esencial

Words you will hear

Estos son los conceptos que presentó esta lección. Cada uno tiene su tarjeta con la definición completa.

HTTPHyperText Transfer ProtocolEl idioma en que dos programas se piden y se responden cosas por la red: un cliente manda una petición (request) y el servidor contesta con una respuesta. Las dos son texto con una forma fija: una primera línea, unos encabezados (headers), una línea en blanco y el contenido (body).Más

Nació con la web para pedir páginas y hoy es el idioma por omisión entre programas: apps, APIs y servidores de modelos. Siempre empieza el cliente, y cada request recibe exactamente una respuesta. La primera línea de la request dice el método, la ruta y la versión; la de la respuesta, la versión y el código de estado. Después van los headers, una línea en blanco y el body. HTTP no tiene memoria (stateless): cada request trae todo lo que hace falta, y por eso cualquier copia de un servidor puede atender cualquier request. Viaja sobre una conexión TCP (o sobre QUIC, en HTTP/3); cifrado dentro de TLS se llama HTTPS. Las versiones que vas a ver: HTTP/1.1, en texto, y HTTP/2, en binario.

Analogía. Una ventanilla con formulario: llenas siempre los mismos renglones en el mismo orden, y te devuelven otro formulario con un sello de «aprobado» o «rechazado». Se rompe en que la ventanilla no te recuerda: cada vez tienes que volver a decir quién eres.

Ejemplo. El backend de C01 escribe «POST /v1/classify?top_k=5 HTTP/1.1», cuatro headers, una línea en blanco y los 214,836 bytes de la foto. El gateway contesta «HTTP/1.1 200 OK», sus headers y un JSON con 5 categorías.

Ver en el glosario →

URLUniform Resource LocatorLa dirección completa de algo en la red, como https://classifier.internal/v1/classify?top_k=5. Dice con qué protocolo hablar, a qué máquina, en qué puerto, qué cosa y con qué parámetros.Más

Tiene cinco partes: el esquema (http o https), el host (el nombre de la máquina, que el DNS convierte en IP), el puerto (opcional: si falta, 80 para http y 443 para https), la ruta (/v1/classify, que el servidor interpreta como quiera) y la query (top_k=5, pares nombre=valor después del ?, separados por &). En la request, la ruta y la query van en la primera línea y el host en el header Host; el esquema y el puerto ya se usaron para abrir la conexión. Un #fragmento nunca sale del navegador. Los caracteres especiales se escriben con % y su código: un espacio es %20 y la ñ, %C3%B1.

Ejemplo. http://localhost:8000/v2/health/ready: esquema http, host localhost, puerto 8000 (el HTTP de Triton), ruta /v2/health/ready y sin query.

Ver en el glosario →

Método HTTPHTTP method · verbLa palabra con que empieza una request y dice qué quieres hacer: GET (dame esto), POST (toma esto y procésalo), PUT (déjalo así), PATCH (cambia esta parte) o DELETE (bórralo).Más

Dos propiedades los separan. Un método es seguro si no cambia nada en el servidor: GET, HEAD y OPTIONS. Es idempotente si hacerlo dos veces deja al servidor igual que hacerlo una: los seguros, más PUT y DELETE. POST y PATCH no lo son: dos POST iguales pueden crear dos cosas. La idempotencia habla del efecto, no de la respuesta: un segundo DELETE responde «no existe», pero el recurso queda igual de borrado. De esto depende qué se puede reintentar sin miedo. Un método que la ruta no acepta recibe un 405 con el header Allow.

Ejemplo. C01 clasifica con POST aunque no cambie nada en el servidor: la foto tiene que ir en el body, y el estándar no le da ningún significado al body de un GET. Un GET a /v1/classify recibe 405 y Allow: POST.

Ver en el glosario →

Headerheader · encabezadoUna línea de datos sobre un mensaje de HTTP, con la forma Nombre: valor, entre la primera línea y la línea en blanco de una petición o una respuesta. Dice a quién va, quién la manda, qué trae el body y cuánto mide.Más

El nombre no distingue mayúsculas, y en HTTP/2 siempre va en minúsculas. Los más comunes: Host (a qué nombre va dirigida), Content-Type (qué hay en el body, como tipo MIME: image/jpeg, application/json), Content-Length (cuántos bytes mide el body), Authorization (la credencial, como Bearer y un JWT), Accept, User-Agent, Retry-After (en un 429 o un 503: cuánto esperar) y Location. Se pueden inventar, como X-Request-ID. Los intermediarios los leen para decidir: un balanceador de carga puede rutear por un header. Pero un header que escribe el cliente sobre sí mismo puede mentir: solo una credencial verificada sirve para dar permisos. Las cookies también son headers.

Analogía. Lo que va escrito por fuera de un sobre: destinatario, remitente, contenido, peso. Se rompe en que, sin HTTPS, la carta de adentro viaja igual de legible que el sobre.

Ejemplo. Content-Length: 214836 deja que el gateway de C01 responda 413 a una foto de más de 10 MB antes de leer un solo byte del body.

Ver en el glosario →

Bodybody · cuerpo · payloadEl contenido de una petición o una respuesta de HTTP: la foto, el JSON o lo que sea que se manda. Va después de los encabezados (headers) y una línea en blanco; el header Content-Type dice su formato y Content-Length, cuántos bytes mide.Más

Puede estar vacío, como en un GET. Una foto se puede mandar de tres formas: en bytes crudos (lo que hace C01, con Content-Type: image/jpeg), dentro de un JSON en base64 (un tercio más grande, y con CPU para codificar y decodificar), o en multipart/form-data, la forma de los formularios web, que separa la foto de otros campos con una frontera. Un body sin tope es una invitación al abuso: por eso cada pieza del camino tiene un tamaño máximo y responde 413 si se pasa. Si el cliente no sabe el tamaño de antemano, lo manda en pedazos (Transfer-Encoding: chunked), y el servidor tiene que contar mientras lee.

Ejemplo. La foto del celular de Ana, 200,000 bytes en crudo, ocupa 266,668 en base64. Al pico de 500 por segundo son 0.8 Gb/s contra ≈ 1.07 Gb/s.

Ver en el glosario →

Código de estadostatus codeEl número de tres cifras de la primera línea de una respuesta de HTTP. Dice qué pasó: 2xx salió bien, 3xx está en otro lado, 4xx tu request está mal y 5xx el servidor falló.Más

La primera cifra es la familia, y con ella se sabe de quién es la culpa. Un 4xx (400, 401, 403, 404, 405, 413, 415, 422) va a fallar igual si se repite, así que no se reintenta y no cuenta contra el SLO; la excepción es el 429 (te pasaste de tu cuota), que se reintenta después de Retry-After. Un 5xx dice que el servidor falló: el 503 (estoy lleno o me falta algo) se reintenta después de Retry-After; el 502 y el 504 los responde un intermediario cuando quien está detrás contesta mal o no contesta a tiempo; el 500 suele ser un bug. El 202 dice «lo recibí, el resultado después». Hay convenciones fuera del estándar, como el 499 de nginx: el cliente se fue antes de la respuesta.

Ejemplo. C01: 413 si la foto pasa de 10 MB, 415 si no es JPEG, PNG ni WebP, 422 si top_k vale 0, 503 si Triton no está disponible y 504 si no contesta en 150 ms. Con un 503 o un 504, el backend muestra el selector manual.

Ver en el glosario →

HTTPSHTTP Secure · HTTP over TLSEl mismo HTTP, metido dentro de TLS, una capa que cifra la conexión y comprueba que el servidor es quien dice ser. Los mensajes no cambian; solo viajan cifrados. Usa el puerto 443.Más

Sin HTTPS, cualquiera en el camino puede leer y cambiar la request: el token de Authorization, la foto, la respuesta. Con TLS, por fuera solo se ve a qué IP y puerto vas, cuánto pesan los mensajes y, casi siempre, el nombre del host; la ruta, los headers y el body quedan ocultos. Cuesta un saludo más al abrir la conexión: 1 RTT con TLS 1.3 y 2 con TLS 1.2, que el keep-alive paga una sola vez. Un balanceador puede terminar TLS: descifra y sigue por la red privada, como el ALB de C01.

Ejemplo. Desde un celular con un RTT de 50 ms, la primera request por HTTPS espera 100 ms de saludos (TCP y TLS 1.3) antes de mandarse.

Ver en el glosario →

Keep-alivekeep-alive · persistent connectionDejar abierta la conexión después de una respuesta para mandar por ella la siguiente request, en vez de abrir una nueva. Ahorra los saludos de TCP y de TLS en cada request.Más

En HTTP/1.1 es lo normal: la conexión sigue abierta salvo que alguien mande Connection: close. Cada lado la cierra cuando pasa un rato sin uso, su idle timeout. Una conexión nueva cuesta 1 RTT de handshake de TCP y 1 o 2 de TLS antes del primer byte; reusarla los ahorra. Los idle timeouts tienen que cuadrar: si el servidor de atrás cierra antes que el balanceador de adelante, el balanceador a veces manda una request por una conexión muerta y responde 502. Por eso en C01 uvicorn espera 75 s detrás de un ALB que espera 60. Como en HTTP/1.1 cada conexión lleva una request a la vez, hace falta un pool para varias al mismo tiempo.

Ejemplo. Con un RTT de 50 ms y TLS 1.3, la primera request tarda 3 RTT más lo del servidor (190 ms con 40 ms de servidor), y las siguientes por la misma conexión, 1 RTT más lo del servidor (90 ms).

Ver en el glosario →

Pool de conexionesconnection poolUn grupo de conexiones abiertas que un cliente guarda y presta: cada request toma una libre, la usa y la devuelve. Ahorra abrir una conexión por request y pone un tope a cuántas van a la vez.Más

En HTTP/1.1, una conexión lleva una request a la vez, así que 16 llamadas simultáneas necesitan 16 conexiones. El pool las mantiene abiertas con keep-alive. Si no hay una libre y no llegó a su máximo, abre otra; si llegó, la request espera su turno, y así el pool limita la concurrencia hacia un servidor. En Python lo traen requests.Session, httpx.Client y http.client.HTTPConnection (una conexión); se crea uno por proceso y se reusa, nunca uno por request. Un pool que no renueva sus conexiones las deja pegadas a los mismos servidores: el que acaba de llegar recibe poco tráfico.

Ejemplo. El gateway de C01 crea un solo cliente de Triton por proceso, con su pool: reusa las conexiones y no hace un handshake por cada foto.

Ver en el glosario →

HTTP/2HTTP/2La versión de HTTP de 2015: el mismo significado (métodos, headers, códigos), pero en binario y con muchas requests a la vez, intercaladas, en una sola conexión. Es la base de gRPC, un sistema para que un programa llame funciones de otro por la red.Más

Cambia el cableado, no el significado. Los mensajes viajan en frames, pedacitos binarios con un encabezado que dice a qué request pertenecen. Cada request es un stream numerado, y los frames de distintos streams se intercalan: es la multiplexación. Los headers se comprimen (HPACK) recordando los que ya se mandaron, y la primera línea se vuelve headers especiales como :method, :path y :status. Una consecuencia importante: un balanceador que reparte conexiones, como un Service de Kubernetes, ya no reparte requests, porque todas van por la misma conexión. Sigue sobre TCP, así que un paquete perdido frena a todos los streams; HTTP/3 lo resuelve con QUIC, sobre UDP.

Ejemplo. gRPC viaja sobre HTTP/2; Triton lo atiende en el puerto 8001. Con una sola conexión larga por gateway, un Triton nuevo puede quedarse casi sin tráfico (M06 y M07).

Ver en el glosario →

14

Chequeo

Esencial

Check

Intenta responder en tu cabeza antes de abrir cada respuesta.

  1. 1¿Cuáles son las cuatro partes de una request de HTTP, en orden?Ver respuesta

    La línea de la petición (método, ruta con query y versión), los headers, una línea en blanco y el body. La respuesta tiene la misma forma, con la línea de estado en vez de la de la petición.

  2. 2¿Cómo sabe el servidor dónde terminan los headers y dónde termina el body?Ver respuesta

    Los headers terminan en la línea en blanco, \r\n\r\n. El body termina cuando se leyeron los bytes que dice Content-Length (o, con chunked, cuando llega un pedazo de tamaño 0).

  3. 3¿Qué significa que un método sea idempotente? ¿DELETE lo es?Ver respuesta

    Que hacerlo dos veces deja al servidor igual que hacerlo una. DELETE sí: el segundo puede responder 404, pero el resultado es el mismo, el recurso borrado. POST no.

  4. 4¿Qué familia de códigos se reintenta y cuál no? ¿Por qué?Ver respuesta

    Los 4xx no: la request está mal y fallaría igual. Algunos 5xx sí (503, 502, 504, con cuidado), y también el 429 aunque sea un 4xx, siempre después de Retry-After.

  5. 5¿En qué se diferencian un 429 y un 503?Ver respuesta

    El 429 dice «te pasaste de tu cuota»: la culpa es del cliente. El 503 dice «estoy saturado o me falta algo»: la culpa es del servidor. Mezclarlos ensucia la alarma de disponibilidad.

  6. 6¿Qué ahorra keep-alive y por qué hace falta un pool?Ver respuesta

    Ahorra el handshake de TCP y el de TLS en cada request: 2 o 3 RTT. En HTTP/1.1 cada conexión lleva una request a la vez, así que para varias al mismo tiempo hace falta un pool de conexiones abiertas.

Sigue · 23APIs: REST, JSON, gRPC y streamingLa red y la web Léelo en la RondaM02 · Patrones de inferenciaLo que acabas de leer, con el ojo del entrevistador. ← 21 · Cómo viaja un mensaje: IP, puertos y DNS