Resiliencia: retries, fallbacks y load balancing
Resiliencia: retries, fallbacks y load balancing
En el capítulo 6 aprendimos a sacarle todo el jugo a la inferencia: streaming, tool calling, multimodal y reranking. Pero hasta ahora hemos asumido algo peligroso: que el proveedor siempre responde. En la vida real, OpenAI devuelve un 429 justo cuando tu producto está en portada de Hacker News, Anthropic tiene una incidencia de 20 minutos, una de tus claves agota su cuota a media tarde. Si tu aplicación habla directo con un único proveedor, cada uno de esos eventos es un incidente para tus usuarios.
La promesa de este capítulo es contundente: con la configuración correcta, tu aplicación percibe cero downtime aunque por debajo haya proveedores cayéndose, claves agotándose y cuotas saturándose. Bifrost convierte ese caos en algo invisible mediante cuatro mecanismos que se apilan: retries (reintentar el mismo proveedor), fallbacks (saltar a otro proveedor/modelo), load balancing (repartir carga entre varias claves) y routing (decidir a dónde va cada request según reglas). Vamos a montarlos uno a uno, entendiendo el porqué de cada decisión.
El mapa de la resiliencia
Antes de tocar configuración conviene tener clara la jerarquía. Cuando llega un request, Bifrost lo procesa en capas: primero decide a qué proveedor lo manda (routing y governance), luego elige qué clave usa dentro de ese proveedor (load balancing), intenta la llamada con su presupuesto de reintentos, y si el proveedor completo se agota, pasa al siguiente de la cadena de fallbacks.
flowchart TD
A["Request entra al gateway"] --> B{"Routing rules / governance<br/>elige proveedor"}
B --> C["Load balancing<br/>elige clave por peso"]
C --> D["Intento con proveedor primario"]
D --> E{"Resultado"}
E -->|"Exito"| F["Respuesta al cliente"]
E -->|"Error reintentable"| G{"Quedan retries?"}
G -->|"Si"| H["Backoff exponencial<br/>+ jitter"]
H --> C
G -->|"No"| I{"Hay fallbacks?"}
I -->|"Si"| J["Siguiente proveedor/modelo<br/>con su propio budget de retries"]
J --> C
I -->|"No"| K["Error final al cliente"]
E -->|"Error de cliente 400/404/422"| K
Cada capa es independiente y configurable. Las veremos en este mismo orden: primero los retries (la defensa más cercana al proveedor), luego los fallbacks, después el load balancing entre claves y, por último, el routing que decide el punto de partida.
Retries: reintentar antes de rendirse
La mayoría de fallos de un proveedor LLM son transitorios: un pico de latencia, un 429 momentáneo, una conexión que se cae. Reintentar con una pequeña espera resuelve un porcentaje altísimo de estos casos sin que el usuario se entere. Bifrost implementa esto con una política de backoff exponencial con jitter, configurable por proveedor dentro de network_config.
Los tres campos de la política
La política de reintentos usa tres parámetros, todos dentro de network_config:
max_retries(entero, por defecto0): número de intentos adicionales tras el primer fallo. Ojo: con el valor por defecto0, no hay reintentos. Hay que activarlos explícitamente.retry_backoff_initial(milisegundos, por defecto500): duración de la espera inicial antes del primer reintento.retry_backoff_max(milisegundos, por defecto5000): techo máximo de la espera; el backoff nunca crece más allá de este valor.
La fórmula del backoff
Bifrost calcula la espera de cada reintento así:
backoff = min(retry_backoff_initial × 2^attempt, retry_backoff_max) × jitter(0.8–1.2)
El componente 2^attempt es lo que hace que cada reintento espere aproximadamente el doble que el anterior (backoff exponencial). El min(..., retry_backoff_max) impone el techo. Y el jitter(0.8–1.2) añade una variación aleatoria del ±20% para evitar el thundering herd: si mil requests fallaran a la vez y todos reintentaran exactamente al mismo milisegundo, volverían a saturar al proveedor en sincronía. El jitter los desfasa.
Con los valores por defecto, la espera escala desde ~400–600 ms en el primer reintento hasta los 4–5 segundos a partir del quinto.
Cuándo se reintenta (y cuándo no)
Esto es clave para no malgastar reintentos en errores que nunca van a resolverse solos. Bifrost reintenta ante:
- Errores de red (DNS, conexión rechazada).
- Errores de servidor
5xx. - Rate limits
429. - Fallos de autenticación
401/403. - Fallos de facturación
402.
Bifrost NO reintenta ante:
- Errores de cliente:
400,404,422. Un request malformado seguirá malformado por mucho que lo repitas. - Bloqueos de plugins (por ejemplo, un plugin de governance que rechaza el request).
El motivo es de sentido común: reintentar un 400 solo añade latencia y consume cuota sin ninguna posibilidad de éxito.
Rotación de claves durante los reintentos
Aquí Bifrost es inteligente. Cuando hay varias claves configuradas para un proveedor (lo veremos en la sección de load balancing), el comportamiento del reintento depende del tipo de error:
- Rate limit (
429): rota a una clave nueva con backoff. La cuota puede ser compartida a nivel de cuenta, así que esperar igual ayuda. - Auth / facturación (
401/402/403): rota a otra clave sin backoff, y marca la clave que falló como muerta permanentemente (no tiene sentido reintentar una clave revocada). - Errores de servidor transitorios: reutiliza la misma clave con backoff, porque el problema es del proveedor, no de la clave.
Configurar retries
En la Web UI (ver capítulo 3): ve a Providers, selecciona el proveedor, abre la sección Network Config y rellena Max Retries, Retry Backoff Initial y Retry Backoff Max.
Vía API con curl:
curl --location 'http://localhost:8080/api/providers' \
--header 'Content-Type: application/json' \
--data '{
"provider": "openai",
"keys": [{"name": "openai-key-1", "value": "env.OPENAI_API_KEY", "models": ["*"], "weight": 1.0}],
"network_config": {
"max_retries": 3,
"retry_backoff_initial": 500,
"retry_backoff_max": 5000
}
}'
Y de forma declarativa en config.json:
{
"providers": {
"openai": {
"keys": [
{ "name": "openai-key-1", "value": "env.OPENAI_KEY_1", "models": ["*"], "weight": 1.0 },
{ "name": "openai-key-2", "value": "env.OPENAI_KEY_2", "models": ["*"], "weight": 1.0 }
],
"network_config": {
"max_retries": 3,
"retry_backoff_initial": 500,
"retry_backoff_max": 5000
}
}
}
}
Con esto, ante un 429 o un 503 Bifrost reintentará hasta 3 veces con backoff creciente antes de considerar al proveedor agotado. Solo entonces entra en juego el siguiente nivel de defensa.
Fallbacks: saltar a otro proveedor cuando el primario muere
Los reintentos resuelven fallos transitorios del mismo proveedor. Pero, ¿qué pasa si el proveedor entero está caído? Reintentar mil veces contra un OpenAI que tiene una incidencia global no sirve de nada. Para eso están los fallbacks: una lista ordenada de pares provider/model que Bifrost prueba en secuencia cuando el primario se agota.
Definir fallbacks por request
A diferencia de los retries (que se configuran en el proveedor), los fallbacks se pasan por request, en el campo fallbacks del body. Esto te da control granular: un endpoint crítico puede tener una cadena de tres fallbacks, mientras que uno secundario va sin red.
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-4o-mini",
"messages": [{"role": "user", "content": "Explain quantum computing"}],
"fallbacks": [
"anthropic/claude-3-5-sonnet-20241022",
"bedrock/anthropic.claude-3-sonnet-20240229-v1:0"
],
"max_tokens": 1000,
"temperature": 0.7
}'
En este ejemplo, el primario es openai/gpt-4o-mini. Si OpenAI agota su presupuesto de reintentos, Bifrost prueba anthropic/claude-3-5-sonnet-20241022; si Anthropic también falla, cae a Claude vía bedrock. Fíjate en algo importante: los fallbacks pueden cruzar proveedores distintos y modelos distintos. No estás limitado a “el mismo modelo en otro proveedor”; puedes degradar a un modelo más barato o cambiar de familia por completo.
Cómo Bifrost identifica al ganador
La respuesta incluye metadata que te dice quién atendió finalmente el request, en extra_fields.provider, junto con extra_fields.latency para el tiempo de respuesta. Esto es oro para observabilidad (lo veremos en el capítulo 11): puedes detectar que tu primario está fallando con frecuencia porque cada vez más respuestas llegan etiquetadas con el proveedor de fallback.
El presupuesto de reintentos se multiplica
Hay un detalle de ejecución que conviene tener muy presente para no llevarse sorpresas con la latencia. Cada proveedor de la cadena recibe su propio presupuesto completo de reintentos. La documentación lo expresa de forma directa: un primario con max_retries: 3 y dos fallbacks, cada uno también con max_retries: 3, significa hasta 12 intentos totales antes de rendirse del todo.
sequenceDiagram
participant App
participant Bifrost
participant OpenAI as openai/gpt-4o-mini
participant Anthropic as anthropic/claude-3-5-sonnet
participant Bedrock as bedrock/claude-3-sonnet
App->>Bifrost: POST /v1/chat/completions (con fallbacks)
Bifrost->>OpenAI: intento 1
OpenAI-->>Bifrost: 503
Bifrost->>OpenAI: reintentos 2-4 (backoff)
OpenAI-->>Bifrost: 503 (budget agotado)
Note over Bifrost: Primario agotado, salto a fallback 1
Bifrost->>Anthropic: intento 1 (budget nuevo)
Anthropic-->>Bifrost: 200 OK
Bifrost-->>App: respuesta + extra_fields.provider = anthropic
Note over Bedrock: nunca se usa: el fallback 1 tuvo exito
Esto es deliberado: cada proveedor merece una oportunidad justa antes de descartarlo. Pero implica que debes dimensionar max_retries con cabeza si la latencia te importa. Una cadena larga con muchos reintentos por eslabón puede tardar bastante en el peor caso.
Plugins y el control de la cadena
Cuando se disparan los fallbacks, los plugins (caching, governance, logging) se ejecutan de nuevo y desde cero para cada proveedor. Esto tiene sentido: el caché podría tener un hit para el modelo de fallback, o la governance podría aplicar límites distintos al segundo proveedor. Además, un plugin puede cortar la cadena en seco poniendo AllowFallbacks = false, deteniendo cualquier salto a fallbacks posteriores. Profundizaremos en plugins en el capítulo 12.
Trazabilidad
Todas las transiciones de retry y fallback se registran en el motor de routing core: rotaciones de clave (se loguea el nombre de la clave, nunca el secreto), evaluaciones de fallback y puntos de agotamiento. Lo hace sin exponer ni los mensajes del proveedor upstream ni las credenciales, lo cual es relevante para auditoría y cumplimiento.
Load balancing: múltiples claves por proveedor
Hasta aquí hemos hablado de saltar entre proveedores. Pero dentro de un mismo proveedor también necesitamos resiliencia y reparto de carga, y ahí entran las múltiples claves por proveedor. ¿Por qué querrías varias claves del mismo proveedor? Para repartir cuota (cada clave de OpenAI tiene su propio límite de RPM/TPM), para aislar entornos, y para tener failover automático si una clave se revoca o agota.
La estructura de una clave
Cada clave se configura como un objeto con estos campos:
name: identificador lógico de la clave.value: el secreto real. Soporta la sintaxisenv.VARIABLE_NAMEpara leerlo de una variable de entorno y no escribirlo en plano en elconfig.json.weight: valor numérico que determina la probabilidad de selección en el balanceo.models: array de modelos soportados. Un array vacío[]deniega todos (deny-by-default desde v1.5.0); usa["*"]para permitir todos.blacklisted_models(opcional): denylist que, si no está vacía, gana sobre la allowlist.
Selección aleatoria ponderada
Bifrost usa weighted random selection para repartir requests entre las claves elegibles. El algoritmo es:
- Calcular el peso total de las claves elegibles.
- Generar un número aleatorio entre 0 y ese peso total.
- Seleccionar la clave según los rangos de peso acumulado.
- Hacer auto-failover a la siguiente clave disponible si la seleccionada falla.
El reparto es directamente proporcional a los pesos. Con dos claves de peso 0.7 y 0.3, la primera recibe ~70% de los requests y la segunda ~30%. Esta selección es extremadamente barata (del orden de nanosegundos), así que no introduce latencia perceptible en el camino crítico.
{
"providers": {
"openai": {
"keys": [
{ "name": "openai-prod-a", "value": "env.OPENAI_KEY_A", "models": ["*"], "weight": 0.7 },
{ "name": "openai-prod-b", "value": "env.OPENAI_KEY_B", "models": ["*"], "weight": 0.3 }
],
"network_config": {
"max_retries": 3,
"retry_backoff_initial": 500,
"retry_backoff_max": 5000
}
}
}
}
Filtrado de modelos por clave
El campo models no es solo decorativo: filtra qué claves son elegibles para cada request. La lógica es:
- Array vacío
[]: deniega todos los modelos (deny-by-default desde v1.5.0). - Array poblado: la clave solo soporta los modelos listados.
- Blacklist con precedencia: si
blacklisted_modelstiene entradas, la denylist gana sobre la allowlist. - Si el modelo del request no coincide, esa clave queda excluida de la selección para ese request.
Esto te permite, por ejemplo, tener una clave de alto tier que solo sirve gpt-4o y otra de bajo coste para los modelos mini, y Bifrost elige automáticamente la adecuada según el modelo pedido.
Forzar una clave concreta
A veces no quieres balanceo, sino una clave específica (debugging, tests, aislamiento de un cliente). Puedes referenciarla y saltarte la selección ponderada:
- Por ID: cabecera
x-bf-api-key-id: <key-id>(o, en el Go SDK, el context valueschemas.BifrostContextKeyAPIKeyID). - Por nombre: cabecera
x-bf-api-key: <key-name>(o el context valueschemas.BifrostContextKeyAPIKeyName).
Cuando se proporciona un ID o nombre explícito, la selección ponderada se omite y se usa directamente la clave referenciada.
Nota de versiones: el antiguo “Direct Key Bypass” (pasar tu propia clave de proveedor saltándote la gestión de Bifrost) fue eliminado por completo en v1.5, tanto del gateway HTTP como del Go SDK. Todos los requests deben usar claves de proveedor gestionadas por Bifrost.
Provider routing: ¿a qué proveedor va cada request?
Las dos secciones anteriores asumían que ya sabíamos a qué proveedor mandar el request. Pero esa decisión también la toma Bifrost, mediante un sistema de routing en capas que prioriza las reglas explícitas sobre la optimización automática.
El formato provider/model
Los requests usan el formato estandarizado provider/model, por ejemplo openai/gpt-4o, azure/gpt-4o o anthropic/claude-3-5-sonnet. Cuando especificas el proveedor en el prefijo, le estás diciendo a Bifrost exactamente a dónde ir.
Si en cambio mandas un nombre de modelo a secas (gpt-4o, sin prefijo), Bifrost resuelve el proveedor automáticamente consultando su Model Catalog, un registro interno de qué modelos sirve cada proveedor. Ambas formas son equivalentes cuando hay un único proveedor candidato:
# Nombre de modelo a secas: Bifrost resuelve el proveedor
{"model": "gpt-4o"}
# Proveedor explicito (resuelve al mismo resultado)
{"model": "openai/gpt-4o"}
Las capas de decisión
Cuando llega un request, Bifrost lo enruta atravesando estas capas, de mayor a menor prioridad:
- Routing rules (expresiones CEL, máxima prioridad): la veremos en la siguiente sección.
- Governance routing (configuraciones explícitas de Virtual Keys): cuando una VK define sus
provider_configs, esa preferencia manda. La governance tiene precedencia porque el usuario la definió explícitamente. La cubrimos en el capítulo 9. - Adaptive load balancing (basado en métricas de rendimiento): es una feature Enterprise. Puntúa proveedores y claves por tasas de error, latencia y utilización, y crea fallbacks ordenados por rendimiento.
- Model Catalog resolver (último recurso): resuelve el proveedor a partir del nombre de modelo cuando no hay nada más.
El adaptive load balancing descrito en el punto 3 (selección de proveedor y clave guiada por métricas en tiempo real, con exploración de claves en recuperación) es exclusivo de la edición Enterprise. El load balancing ponderado por pesos estáticos que vimos en la sección anterior sí está en la versión open source.
Routing ponderado vía governance
Dentro de la governance puedes definir varios proveedores para un mismo modelo, cada uno con un peso, y dejar que Bifrost reparta. Por ejemplo, mandando el 70% del tráfico a Azure y el 30% a OpenAI:
{
"provider_configs": [
{
"provider": "openai",
"allowed_models": ["gpt-4o", "gpt-4o-mini"],
"weight": 0.3
},
{
"provider": "azure",
"allowed_models": ["gpt-4o"],
"weight": 0.7
}
]
}
Con esta configuración, ~70% de los requests resuelven a azure/gpt-4o y ~30% a openai/gpt-4o, y el proveedor no elegido se añade como fallback. Es la base de patrones de optimización de coste muy potentes (por ejemplo, mandar el 99% del tráfico a un proxy barato como OpenRouter y dejar el 1% en el proveedor directo como red de seguridad). El detalle completo de provider_configs, allowed_models, presupuestos y rate limits pertenece al capítulo 9.
Routing rules: enrutar según reglas dinámicas
El nivel más fino y potente del routing son las routing rules: expresiones CEL (Common Expression Language) que se evalúan en tiempo de request, antes de la selección de proveedor por governance, y que pueden sobreescribir esa selección por completo. Sirven para escenarios como “los usuarios premium van siempre al proveedor más rápido” o “si el presupuesto está al 85%, degrada a un modelo más barato”.
Anatomía de una regla
Cada routing rule contiene estos campos:
name(requerido): identificador único dentro del scope.description(opcional): documentación interna.enabled: booleano para activar/desactivar.chain_rule: cuando estrue, tras hacer match re-evalúa todas las reglas usando el proveedor/modelo ya resuelto como nuevo contexto.cel_expression: la condición CEL a evaluar.targets(requerido): array de destinos ponderados, cada uno conprovider(opcional),model(opcional),key_id(opcional, UUID de una clave, requiereprovider) yweight(requerido; todos los pesos deben sumar1.0).fallbacks(opcional): array de strings con formatoprovider/model.scope:"global","customer","team"o"virtual_key".scope_id: requerido si el scope no es global.priority: orden ascendente (el0se evalúa antes que el10).
Variables CEL disponibles
Las expresiones tienen acceso a un contexto rico del request:
- Request:
model,provider,request_type(chat_completion,embedding,batch,image_generation,moderation,transcription,translation). - Cabeceras y parámetros:
headers["header-name"](búsqueda insensible a mayúsculas),params["param-name"]. - Organización:
virtual_key_id,virtual_key_name,team_id,team_name,customer_id,customer_name. - Métricas de capacidad (porcentajes 0-100):
budget_used,tokens_used,request. - Clasificación de complejidad:
complexity_tier("SIMPLE","MEDIUM","COMPLEX","REASONING").
Y operadores y funciones de cadena habituales: ==, !=, >, <, &&, ||, !, .startsWith(), .endsWith(), .contains(), .matches(), y in para colecciones.
Ejemplo: ruta para clientes premium
{
"governance": {
"routing_rules": [
{
"id": "rule-uuid-123",
"name": "Premium Tier Route",
"description": "Route premium users to fast provider",
"enabled": true,
"chain_rule": false,
"cel_expression": "headers[\"x-tier\"] == \"premium\"",
"targets": [
{ "provider": "openai", "model": "gpt-4o", "weight": 0.7 },
{ "provider": "azure", "model": "gpt-4o", "weight": 0.3 }
],
"fallbacks": ["groq/gpt-3.5-turbo"],
"scope": "global",
"scope_id": null,
"priority": 10
}
]
}
}
Esta regla dice: “si la cabecera x-tier vale premium, reparte 70/30 entre OpenAI y Azure (ambos gpt-4o), y si todo falla, cae a groq/gpt-3.5-turbo”. Fíjate en que la propia regla puede declarar sus fallbacks, integrándose con el mecanismo que vimos antes.
Orden de evaluación y chaining
Las reglas siguen first-match-wins dentro de una jerarquía de scopes, de mayor a menor prioridad: VirtualKey, luego Team, luego Customer y por último Global. Dentro de cada scope se ordenan por priority ascendente (el 0 antes que el 10).
Cuando chain_rule es true, el motor no se detiene tras el primer match: actualiza el contexto con el proveedor/modelo resuelto y re-evalúa todo el conjunto desde arriba. La cadena termina cuando ninguna regla hace match, cuando una regla con match tiene chain_rule: false, o cuando proveedor y modelo dejan de cambiar (detección de convergencia). El chaining de reglas está disponible a partir de Bifrost v1.5.0-prerelease2.
Independientemente de si el proveedor lo decidió una routing rule o la governance, el load balancing de selección de clave siempre se ejecuta después, sobre el proveedor ya determinado.
Performance tuning de proveedores
La resiliencia también depende de que cada proveedor tenga suficiente capacidad para absorber tu carga sin formar colas eternas. Bifrost expone tres palancas de rendimiento por proveedor (y una global) que conviene conocer a alto nivel; el tuning fino para producción lo detallamos en el capítulo 14.
Las palancas
concurrency(por proveedor, por defecto1000): número de goroutines (workers) que procesan requests en paralelo para ese proveedor.buffer_size(por proveedor, por defecto5000): capacidad de la cola de requests entrantes antes de que los workers los recojan.initial_pool_size(global, configurable; en este ejemplo10000): pre-asigna objetos reutilizables en todo el sistema, reduciendo presión sobre el garbage collector. La documentación no fija un valor por defecto universal: ajústalo a tu carga (un punto de partida razonable son unos cientos, p. ej.500, y se sube según el throughput).
La estructura por proveedor en config.json:
"concurrency_and_buffer_size": {
"concurrency": 100,
"buffer_size": 500
}
Y la configuración global:
"config": {
"initial_pool_size": 10000,
"drop_excess_requests": false
}
Dimensionar bien
La documentación ofrece una fórmula de partida basada en tus requests por segundo esperados (RPS): concurrency = expected_rps y buffer_size = 1.5 × expected_rps. Por ejemplo, a 500 RPS por proveedor, pondrías concurrency: 500 y buffer_size: 750.
Hay una restricción crítica que no puedes violar: buffer_size debe ser mayor o igual que concurrency. Si concurrency > buffer_size, el setup del proveedor falla al arrancar.
Qué pasa cuando la cola se llena
El campo global drop_excess_requests controla el comportamiento bajo saturación:
false(por defecto): los requests nuevos esperan a que haya espacio en la cola. Prioriza no perder ningún request, a costa de latencia.true: el sistema rechaza inmediatamente con error cuando la cola se llena. Prioriza fallar rápido (fail-fast) en vez de acumular latencia, lo cual encaja bien con un patrón donde el cliente reintenta o hay fallbacks por encima.
La elección entre ambos depende de tu SLA: para cargas con picos donde prefieres latencia controlada sobre throughput máximo, true evita que la cola crezca sin control.
Resumen
En este capítulo hemos apilado las cuatro capas que convierten un gateway frágil en una infraestructura que tu aplicación percibe como siempre disponible:
- Retries: política de backoff exponencial con jitter, configurable por proveedor con
max_retries,retry_backoff_initialyretry_backoff_maxdentro denetwork_config. Recuerda que el valor por defecto demax_retrieses0: hay que activarlos. Solo se reintentan errores recuperables (5xx,429,401/402/403, red), nunca errores de cliente (400/404/422). - Fallbacks: lista ordenada de pares
provider/modelque se pasa por request en el campofallbacks. Cada eslabón recibe su propio presupuesto de reintentos (cuidado con la latencia acumulada), yextra_fields.providerte dice quién atendió. - Load balancing: múltiples claves por proveedor con
weight, seleccionadas por weighted random selection en nanosegundos, con filtrado pormodels/blacklisted_modelsy auto-failover. Puedes forzar una clave conx-bf-api-key-idox-bf-api-key. - Routing: el formato
provider/model, el Model Catalog para resolver nombres a secas, la governance ponderada y las routing rules con expresiones CEL para decisiones dinámicas. El adaptive load balancing por métricas es Enterprise. - Performance:
concurrencyybuffer_sizepor proveedor (con la reglabuffer_size >= concurrency),initial_pool_sizeglobal ydrop_excess_requestspara elegir entre esperar o fallar rápido.
El mensaje de fondo: con estas capas bien configuradas, un proveedor caído, una clave revocada o un pico de tráfico dejan de ser incidentes visibles. Tu aplicación ve cero downtime percibido porque Bifrost absorbe el caos por debajo.
En el siguiente capítulo daremos un giro hacia el coste y la latencia: el semantic caching, que evita llamar al proveedor cuando una pregunta es semánticamente equivalente a otra ya respondida.
Siguiente: Semantic caching: reduce costo y latencia