Shopify API 限流问题排查

429 错误,API 请求超过限制导致应用功能中断的排查和解决方案

#type / debug #status / growing #tech / dev #resource / shopify

[!info] related notes

Shopify API 限流问题排查

现象

应用调用 Shopify API 时返回 429 Too Many Requests 错误:

{
  "errors": "Exceeded 2 calls per second for api client. Reduce request rates to resume uninterrupted service."
}

影响

  • 应用功能中断
  • 数据同步失败
  • 用户体验下降

根本原因

Shopify 限制 API 调用频率,防止滥用:

REST API 限制

Bucket 算法

  • 填充速率:每秒 2 次请求
  • 桶容量:40 次请求
  • 计算:每次请求消耗 1 个令牌

示例

初始:桶有 40 个令牌
0 秒:发送 40 个请求 → 桶空
2 秒:桶恢复 4 个令牌
发送第 41 个请求 → 如果桶空,返回 429

GraphQL API 限制

基于查询成本

  • 每秒 50 个成本点
  • 桶容量 1000 个成本点
  • 不同查询消耗不同成本

示例查询成本

query {
  products(first: 10) {  # 成本:12
    edges {
      node {
        id
        title
      }
    }
  }
}

解决方案

1. 添加速率限制逻辑

Node.js 示例

import pLimit from 'p-limit';

// 限制并发请求数
const limit = pLimit(2);  // 每秒最多 2 个请求

const promises = products.map(product => 
  limit(() => updateProduct(product))
);

await Promise.all(promises);

2. 检测 429 并重试

async function callShopifyAPI(query, retries = 3) {
  try {
    const response = await admin.graphql(query);
    return await response.json();
  } catch (error) {
    if (error.status === 429 && retries > 0) {
      // 指数退避
      const delay = Math.pow(2, 3 - retries) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
      return callShopifyAPI(query, retries - 1);
    }
    throw error;
  }
}

3. 优化 GraphQL 查询

❌ 低效查询(多次请求):

for (const productId of productIds) {
  const product = await admin.graphql(`
    query {
      product(id: "${productId}") {
        id
        title
      }
    }
  `);
}

✅ 高效查询(单次请求):

query {
  nodes(ids: $productIds) {
    ... on Product {
      id
      title
    }
  }
}

4. 使用批量操作(Bulk Operations)

对于大量数据(10,000+ 产品):

mutation {
  bulkOperationRunQuery(
    query: """
    {
      products {
        edges {
          node {
            id
            title
          }
        }
      }
    }
    """
  ) {
    bulkOperation {
      id
      status
    }
  }
}

优势

  • 不受速率限制
  • 异步处理
  • 适合大量数据导出

5. 缓存数据

const cache = new Map();

async function getProduct(productId) {
  if (cache.has(productId)) {
    return cache.get(productId);
  }
  
  const product = await fetchFromShopify(productId);
  cache.set(productId, product);
  return product;
}

6. 使用 Webhook 替代轮询

❌ 低效(每分钟轮询):

setInterval(async () => {
  const orders = await fetchOrders();
  // 处理新订单
}, 60000);

✅ 高效(使用 Webhook):

// 订阅 orders/create Webhook
app.post('/webhooks/orders', (req, res) => {
  const order = req.body;
  processNewOrder(order);
  res.status(200).send('OK');
});

监控与预防

1. 检查响应头

Shopify 返回当前限流状态:

const response = await fetch(shopifyURL);
console.log(response.headers.get('X-Shopify-Shop-Api-Call-Limit'));
// 输出:32/40(已用 32,剩余 8)

2. 设置告警

if (callLimit > 35) {  // 接近上限
  console.warn('Approaching API rate limit');
  // 发送告警通知
}

3. 使用 GraphQL 成本分析

在 GraphQL 查询中请求成本信息:

query {
  products(first: 10) {
    edges {
      node {
        id
      }
    }
  }
}

# 查看响应中的 extensions.cost

最佳实践

  1. 避免循环中调用 API:批量处理
  2. 使用 Webhook:替代轮询
  3. 缓存数据:减少重复请求
  4. GraphQL 优先:比 REST 更高效
  5. 监控限流状态:提前预警

调试检查清单

  • 是否在循环中调用 API?
  • 是否可以用单次批量查询替代多次查询?
  • 是否可以用 Webhook 替代轮询?
  • 是否添加了速率限制和重试逻辑?
  • 是否监控了 API 调用频率?
创建于 2026/6/15 更新于 2026/6/15