Shopify

Cómo Solucionar el Error 429 Too Many Requests y Throttled en la API de Shopify

Introducción al Error 429 Too Many Requests y Throttled en Shopify

El desarrollo de aplicaciones integradas, scripts de sincronización de inventario y temas personalizados en Shopify exige una interacción constante con sus interfaces de programación de aplicaciones (APIs), como la Admin REST API, la Admin GraphQL API y la Storefront API. Uno de los obstáculos técnicos más severos en entornos de producción de alto volumen es la aparición del error HTTP 429 Too Many Requests o la respuesta GraphQL con el código de error THROTTLED.

Shopify implementa un estricto mecanismo de control de tráfico denominado Leaky Bucket Algorithm (algoritmo de cubo con fugas) para garantizar la estabilidad de su infraestructura multitenant. Cuando una aplicación o script sobrepasa la capacidad del cubo asignado a la tienda o al token de acceso, la plataforma bloquea temporalmente las peticiones entrantes. Superar este problema requiere ir más allá de simples pausas fijas; exige comprender la métrica de costo de las consultas GraphQL, implementar patrones de reintento exponencial con fluctuación (jitter) y diseñar arquitecturas de cola orientadas a eventos.

Análisis Profundo de las Causas del Límite de Tasa en Shopify

Para mitigar eficazmente el estrangulamiento de peticiones, es fundamental comprender las diferencias operativas entre la arquitectura REST y GraphQL en Shopify:

  • Límites en la Admin REST API: Funciona mediante un cubo de capacidad fija (habitualmente 40 peticiones para tiendas estándar y 80 para tiendas Shopify Plus) que se vacía a una velocidad constante de 2 peticiones por segundo (4 peticiones/segundo en Shopify Plus). Si el ritmo de peticiones supera la tasa de fuga, el cubo se desborda y devuelve inmediatamente un código de estado HTTP 429.
  • Límites en la Admin GraphQL API: No mide el número absoluto de peticiones HTTP, sino el costo computacional calculado de cada consulta. El cubo tiene una capacidad máxima de puntos (por ejemplo, 1,000 puntos en tiendas estándar) y se restaura a una velocidad constante (por ejemplo, 50 puntos por segundo). Consultas anidadas profundamente con múltiples relaciones (como productos con variantes, colecciones y metafields) consumen cientos de puntos en una sola ejecución.
  • Peticiones Concurrenciales en el Frontend (Storefront API / Cart API): Disparar eventos AJAX masivos desde el navegador sin técnicas de aceleración (debouncing) cuando múltiples usuarios interactúan con el carrito de compras o los filtros en vivo puede agotar la cuota asignada por dirección IP.
  • Falta de Manejo de Encabezados de Respuesta: Desestimar la inspección de los encabezados X-Shopify-Shop-Api-Call-Limit (en REST) o del objeto extensions.cost (en GraphQL) impide predecir cuándo el cubo está a punto de desbordarse.

Método 1: Optimización de Consultas GraphQL y Reducción del Requested Cost

El primer paso para evitar el error THROTTLED en GraphQL es reducir el impacto computacional de las consultas ejecutadas. Shopify evalúa el costo máximo teórico de una consulta antes de procesarla. Si el requestedQueryCost supera los puntos disponibles en el cubo en ese instante, la petición es rechazada inmediatamente.

Inspección del Costo de la Consulta

Toda respuesta GraphQL de Shopify incluye metadatos sobre el consumo de puntos dentro del bloque extensions. Analiza este payload para depurar tu código:

{
  "data": {
    "products": {
      "edges": []
    }
  },
  "extensions": {
    "cost": {
      "requestedQueryCost": 102,
      "actualQueryCost": 12,
      "throttleStatus": {
        "maximumAvailable": 1000.0,
        "currentlyAvailable": 898.0,
        "restoreRate": 50.0
      }
    }
  }
}

Estrategias de Reducción de Costo:

  1. Reducir el argumento first: En lugar de solicitar first: 250 items con múltiples niveles de anidamiento, reduce el tamaño de página a first: 50 o first: 100 y utiliza paginación basada en cursores (after).
  2. Eliminar campos innecesarios: Solicita estrictamente los campos requeridos para la operación lógica actual. Evita traer metafields, imágenes e inventarios completos en consultas destinadas únicamente a listar títulos e identificadores.
  3. Utilizar Aliases y Fragmentos de forma prudente: Duplicar nodos dentro de la misma consulta suma el costo de cada campo duplicado al requestedQueryCost total.

Método 2: Implementación de Algoritmo Exponential Backoff con Jitter en Node.js

Cuando una API responde con un error 429 o un código THROTTLED, reintentar la operación de inmediato empeorará la saturación. La solución estándar en la industria es implementar un retraso exponencial combinado con una variación aleatoria (jitter) para desincronizar peticiones concurrentes.

El siguiente script en JavaScript/Node.js encapsula la función de petición nativa o cliente HTTP para gestionar de forma automática los reintentos basándose en el estado de estrangulamiento de Shopify:

async function fetchShopifyGraphQL(endpoint, accessToken, query, variables = {}, attempt = 1) {
  const MAX_ATTEMPTS = 6;
  const BASE_DELAY = 1000; // 1 segundo

  try {
    const response = await fetch(endpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Shopify-Access-Token': accessToken,
      },
      body: JSON.stringify({ query, variables }),
    });

    const responseData = await response.json();

    // Verificar si la API GraphQL devolvió error de estrangulamiento interno
    const isThrottled = responseData.errors && responseData.errors.some(e => e.extensions && e.extensions.code === 'THROTTLED');

    if (response.status === 429 || isThrottled) {
      if (attempt > MAX_ATTEMPTS) {
        throw new Error(`Se superó el límite máximo de reintentos (${MAX_ATTEMPTS}). Petición abortada.`);
      }

      // Extraer tiempo de espera sugerido o calcular retraso exponencial con Jitter
      let waitTime = Math.pow(2, attempt) * BASE_DELAY + Math.random() * 500;
      
      if (responseData.extensions && responseData.extensions.cost) {
        const { currentlyAvailable, restoreRate } = responseData.extensions.cost.throttleStatus;
        if (currentlyAvailable < 100) {
          // Calcular segundos necesarios para regenerar suficientes puntos
          waitTime = Math.max(waitTime, ((100 - currentlyAvailable) / restoreRate) * 1000);
        }
      }

      console.warn(`[Shopify API Rate Limit] Intento ${attempt} fallido. Esperando ${Math.round(waitTime)}ms antes de reintentar...`);
      await new Promise(resolve => setTimeout(resolve, waitTime));
      
      return fetchShopifyGraphQL(endpoint, accessToken, query, variables, attempt + 1);
    }

    return responseData;
  } catch (error) {
    console.error('Error en la llamada a la API de Shopify:', error);
    throw error;
  }
}

Método 3: Gestión de Colas y Control de Concurrencia con Redis y BullMQ

Para aplicaciones empresariales que procesan miles de actualizaciones simultáneas (por ejemplo, sincronización de precios desde un ERP externo), ejecutar llamadas HTTP directas destruirá la cuota de la API. La arquitectura idónea exige encolar los trabajos y procesarlos mediante un worker que respete la tasa de regeneración de Shopify.

A continuación se muestra un ejemplo de configuración de cola utilizando BullMQ en un entorno Node.js con Redis:

const { Queue, Worker } = require('bullmq');
const redisConnection = { host: '127.0.0.1', port: 6379 };

// Crear la cola de sincronización para Shopify
const shopifySyncQueue = new Queue('shopify-api-sync', { connection: redisConnection });

// Configurar el Worker con un límite de tasa (Rate Limiter) estricto
const worker = new Worker('shopify-api-sync', async (job) => {
  const { endpoint, token, query, variables } = job.data;
  
  // Ejecutar llamada a la API usando la función con manejo de backoff
  const result = await fetchShopifyGraphQL(endpoint, token, query, variables);
  return result;
}, {
  connection: redisConnection,
  limiter: {
    max: 40, // Máximo 40 trabajos...
    duration: 1000, // ...por cada 1000 milisegundos (ajustar según el plan de Shopify)
  },
});

worker.on('failed', (job, err) => {
  console.error(`Trabajo ${job.id} falló con error: ${err.message}`);
});

Método 4: Optimización en la Cart API AJAX y Storefront en el Frontend

Si el error 429 se origina en el frontend de la tienda debido a múltiples llamadas simultáneas a los endpoints /cart/add.js, /cart/change.js o a la Storefront API mediante scripts de temas personalizados, se debe implementar una técnica de Debounce para agrupar mutaciones en una sola transacción.

Código JavaScript para evitar colisiones en la API del Carrito:

let cartUpdateTimer = null;

function updateCartItemQuantity(variantId, quantity) {
  // Cancelar ejecución pendiente si el usuario sigue haciendo clic rápidamente
  clearTimeout(cartUpdateTimer);

  cartUpdateTimer = setTimeout(() => {
    fetch('/cart/change.js', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        id: String(variantId),
        quantity: quantity
      })
    })
    .then(response => {
      if (response.status === 429) {
        console.error('Demasiadas peticiones al carrito. Por favor espera unos segundos.');
        return null;
      }
      return response.json();
    })
    .then(cart => {
      if (cart) {
        document.dispatchEvent(new CustomEvent('cart:updated', { detail: cart }));
      }
    })
    .catch(error => console.error('Error al actualizar el carrito:', error));
  }, 400); // Esperar 400ms de inactividad antes de enviar la petición HTTP
}

Conclusión y Buenas Prácticas para Entornos de Producción

El error 429 Too Many Requests o THROTTLED en Shopify no es un error de software insuperable, sino una señal de control de tráfico que exige un diseño de software resiliente. Al migrar tus aplicaciones de REST a GraphQL, optimizar el costo computacional de las consultas, implementar el algoritmo de Exponential Backoff con Jitter y gestionar los flujos de trabajo masivos mediante colas con Redis, garantizarás un funcionamiento ininterrumpido y altamente escalable incluso en períodos de tráfico masivo como Black Friday / Cyber Monday (BFCM).