Introducción al sistema de límites de tasa en la API GraphQL de Shopify
Cuando se desarrollan aplicaciones avanzadas, integraciones con ERPs o scripts de sincronización masiva para Shopify mediante la Admin GraphQL API, uno de los obstáculos técnicos más críticos es la aparición de las respuestas con código de estado 429 Too Many Requests o errores GraphQL con la clave THROTTLED y MAX_COST_EXCEEDED.
A diferencia de la API REST tradicional, que limita las peticiones basándose en un número fijo de solicitudes por segundo (mediante el algoritmo de cubo con fuga o leaky bucket), la API GraphQL de Shopify utiliza un modelo basado en el **costo computacional de la consulta** (Query Cost). Shopify asigna a cada tienda un límite máximo de puntos en su cubo de capacidad (habitualmente 1000 puntos para planes estándar y hasta 2000 o más para Shopify Plus), el cual se restaura a una tasa constante de puntos por segundo (normalmente 50 puntos/segundo). Comprender este mecanismo es vital para diseñar software robusto que mantenga la disponibilidad sin sufrir bloqueos sistemáticos.
Análisis exhaustivo del origen del error THROTTLED y MAX_COST_EXCEEDED
Existen dos variantes principales de este fallo dentro de la respuesta en formato JSON devuelta por el endpoint de GraphQL (/admin/api/YYYY-MM/graphql.json):
1. Error de Solicitud Individual Excesiva (MAX_COST_EXCEEDED)
Este error ocurre antes de que la consulta sea ejecutada. El motor de GraphQL de Shopify calcula sintácticamente el requestedQueryCost (el costo máximo teórico de la consulta si se devolvieran todos los nodos solicitados). Si el costo total calculado supera la capacidad máxima total del cubo de tu API (por ejemplo, solicitar 250 productos, cada uno con 250 variantes y sus respectivas imágenestienen un costo teórico superior a 1000 puntos), el servidor rechazará la consulta inmediatamente con la clave de extensión MAX_COST_EXCEEDED.
2. Agotamiento del Cubo de Puntos (THROTTLED)
Este escenario se produce dinámicamente cuando ejecutas múltiples consultas consecutivas. Aunque cada consulta individual tenga un costo válido (por ejemplo, 100 puntos), si realizas 12 peticiones seguidas en un intervalo de un segundo, el total consumido (1200 puntos) supera la capacidad disponible en el cubo. En ese instante, Shopify devuelve un error de tipo THROTTLED indicando cuántos segundos debes esperar para que la tasa de fuga libere suficientes puntos.
Método 1: Diagnóstico e inspección de cabeceras HTTP y extensiones GraphQL
Para abordar el problema, el primer paso técnico consiste en auditar las respuestas del servidor. Shopify incluye información detallada sobre el consumo de recursos tanto en los encabezados HTTP como en el cuerpo de la respuesta GraphQL bajo el objeto extensions.cost.
Para ver el estado en tiempo real, analiza la carga devuelta en cada petición ejecutada desde cURL, Postman o tu cliente HTTP preferido:
{
"data": {
"products": { "edges": [] }
},
"extensions": {
"cost": {
"requestedQueryCost": 102,
"actualQueryCost": 12,
"throttleStatus": {
"maximumAvailable": 1000.0,
"currentlyAvailable": 898.0,
"restoreRate": 50.0
}
}
}
}
Analiza minuciosamente la diferencia entre requestedQueryCost y actualQueryCost. Si pides datos con límites muy amplios pero sólo existen pocos registros reales, el costo reservado será alto pero el ejecutado será bajo. Sin embargo, el límite inicial se evalúa basándose en el requestedQueryCost, lo que provoca rechazos prematuros.
Método 2: Optimización de la estructura de consultas y paginación con Cursors
La forma más efectiva de reducir el costo de consulta y prevenir el error MAX_COST_EXCEEDED es optimizar el número de nodos solicitados y eliminar campos innecesarios.
- Reduce los valores de los argumentos
firstylast: Solicitarfirst: 250en conexiones anidadas multiplica exponencialmente el costo. Reduce las páginas a bloques de 50 o 100 elementos. - Elimina campos redundantes o anidados innecesarios: No solicites metainformación profunda dentro de un bucle si sólo necesitas el ID y el título.
- Implementa paginación basada en cursores (Relay Cursor Connections): En lugar de intentar traer todo el árbol de datos de una sola vez, utiliza paginación secuencial con
after.
Ejemplo de consulta optimizada para reducir el requestedQueryCost:
query getOptimizedProducts($cursor: String) {
products(first: 50, after: $cursor) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
title
variants(first: 10) {
edges {
node {
id
price
sku
}
}
}
}
}
}
}
Método 3: Implementación de un cliente de reintento automático con Exponential Backoff y Jitter
Cuando trabajas con un flujo continuo de datos, debes abstraer la capa de red para que intercepte las respuestas THROTTLED y pause automáticamente la ejecución según el tiempo necesario para regenerar los puntos.
A continuación se muestra una solución avanzada en JavaScript (Node.js) utilizando una estrategia de espera calculada dinámicamente mediante el campo throttleStatus:
async function fetchGraphQLWithRetry(client, query, variables) {
const MAX_RETRIES = 5;
let attempt = 0;
while (attempt < MAX_RETRIES) {
const response = await client.rawRequest(query, variables);
const { errors, extensions } = response;
if (errors && errors.some(err => err.extensions?.code === 'THROTTLED')) {
attempt++;
const throttleStatus = extensions?.cost?.throttleStatus;
let waitTimeMs = 2000; // Tiempo por defecto
if (throttleStatus) {
const requestedCost = extensions.cost.requestedQueryCost;
const available = throttleStatus.currentlyAvailable;
const restoreRate = throttleStatus.restoreRate;
// Calcular tiempo necesario para recuperar los puntos requeridos
const neededPoints = requestedCost - available;
waitTimeMs = Math.ceil((neededPoints / restoreRate) * 1000) + 100; // +100ms de margen
} else {
// Backoff exponencial si no hay metadata
waitTimeMs = Math.pow(2, attempt) * 1000 + Math.random() * 500;
}
console.warn(`Petición pausada por THROTTLED. Esperando ${waitTimeMs}ms (Intento ${attempt})...`);
await new Promise(resolve => setTimeout(resolve, waitTimeMs));
continue;
}
return response;
}
throw new Error('Número máximo de reintentos alcanzado debido a límites de tasa.');
}
Método 4: Migración a Bulk Operations API para procesamiento masivo de datos
Si la tarea requiere exportar o procesar catálogos completos (por ejemplo, sincronizar 100.000 productos o procesar el historial completo de pedidos), utilizar la API GraphQL síncrona es un error de diseño. Para estos casos de uso, Shopify proporciona la **Bulk Operations API** (Operaciones Masivas).
Las operaciones masivas ejecutan la consulta de forma asíncrona en los servidores de Shopify. No consumen tus puntos de API en tiempo real y devuelven el resultado en un archivo comprimido en formato JSON Lines (JSONL).
Paso 1: Iniciar la consulta masiva
mutation {
bulkOperationRunQuery(
query: """
{
products {
edges {
node {
id
title
variants {
edges {
node {
id
sku
}
}
}
}
}
}
}
"""
) {
bulkOperation {
id
status
}
userErrors {
field
message
}
}
}
Paso 2: Consultar el estado o recibir Webhook de finalización
Puedes suscribirte al webhook BULK_OPERATIONS_FINISH o realizar un sondeo (polling) periódico para obtener la URL de descarga del archivo JSONL generado:
query {
currentBulkOperation {
id
status
errorCode
createdAt
completedAt
objectCount
fileSize
url
}
}
Conclusión y buenas prácticas de arquitectura
Para erradicar definitivamente los fallos por límites de tasa en Shopify, la arquitectura de tu aplicación debe ser resiliente por diseño. Implementa las siguientes buenas prácticas:
- Combina estrategias: Utiliza llamadas GraphQL optimizadas e individuales para interacciones en tiempo real en el frontend o checkout, y migra a la Bulk Operations API para tareas batch de backend.
- Gestión de colas: Implementa sistemas de colas de mensajes (como Redis con BullMQ o RabbitMQ) para regular la tasa de peticiones concurrentes emitidas por tus workers.
- Monitoreo continuo: Registra los valores de
currentlyAvailableyactualQueryCosten tus logs para detectar degradaciones de rendimiento antes de que impacten en producción.