大量追跡時のAPIレート制限ベストプラクティス

ECプラットフォームや物流システムでは、数千から数万の荷物を同時に追跡する必要があります。しかし、何も考えずにAPIを呼び出すと、すぐにレート制限に引っかかってしまいます。本記事では、WhereParcelのレート制限を効率的に管理し、限られたリクエスト数で最大限の成果を得るための5つの戦略をご紹介します。

WhereParcelのレート制限構造

WhereParcelのAPIには、プランごとにレート制限が設定されています。

プランリクエスト/分リクエスト/日バッチサイズ上限Webhook
Free301,00010不可
Starter12010,00050
Business600100,000100
Enterpriseカスタムカスタムカスタム

レート制限を超過すると、APIは 429 Too Many Requests を返します。レスポンスヘッダーには次のリクエストが可能になるまでの待機時間が含まれています:

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1706234567
Retry-After: 45

戦略1:バッチ追跡を活用する

最も効果的な方法は、個別リクエストの代わりにバッチAPIを使用することです。

個別リクエスト(非効率)

// 悪い例:100個の荷物を個別にリクエスト → 100回のAPI呼び出し
const trackingNumbers = ['TN001', 'TN002', /* ... */ 'TN100'];

for (const tn of trackingNumbers) {
  const result = await fetch('https://api.whereparcel.com/v2/track', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${API_KEY}:${SECRET_KEY}`
    },
    body: JSON.stringify({
      trackingItems: [{ carrier: 'kr.cjlogistics', trackingNumber: tn }]
    })
  });
  // 100件 = 100リクエスト消費
}

バッチリクエスト(効率的)

// 良い例:100個の荷物を1回のバッチリクエストで追跡 → 1回のAPI呼び出し
const trackingNumbers = ['TN001', 'TN002', /* ... */ 'TN100'];

// 50件ずつバッチに分割(Starterプランの上限)
const batches = chunkArray(trackingNumbers, 50);

for (const batch of batches) {
  const result = await fetch('https://api.whereparcel.com/v2/track/batch', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${API_KEY}:${SECRET_KEY}`
    },
    body: JSON.stringify({
      parcels: batch.map(tn => ({
        carrier: 'kr.cjlogistics',
        trackingNumber: tn
      }))
    })
  });
  // 100件 = 2リクエスト消費(50件 × 2バッチ)
}

function chunkArray(array, size) {
  const chunks = [];
  for (let i = 0; i < array.length; i += size) {
    chunks.push(array.slice(i, i + size));
  }
  return chunks;
}

効果: 100リクエスト → 2リクエスト(98%削減

戦略2:スマートキャッシングを実装する

すべての荷物を同じ頻度でポーリングする必要はありません。配送ステータスに応じてキャッシュ期間を調整しましょう。

/**
 * ステータスに応じたキャッシュ期間を返す
 */
function getCacheDuration(status) {
  const cacheDurations = {
    'pending':          60 * 60,        // 1時間(まだ集荷されていない)
    'info_received':    30 * 60,        // 30分(情報受信済み)
    'in_transit':       15 * 60,        // 15分(配送中 - 頻繁に更新)
    'out_for_delivery':  5 * 60,        // 5分(配達中 - 最も頻繁に更新)
    'delivered':        24 * 60 * 60,   // 24時間(配達完了 - もう変化しない)
    'exception':        10 * 60,        // 10分(問題あり - 注意深く監視)
    'expired':          Infinity        // キャッシュ永続(期限切れ - 変化しない)
  };
  return cacheDurations[status] || 15 * 60;
}

/**
 * キャッシュを考慮した追跡データ取得
 */
async function getTrackingWithCache(carrier, trackingNumber) {
  const cacheKey = `tracking:${carrier}:${trackingNumber}`;

  // キャッシュを確認
  const cached = await cache.get(cacheKey);
  if (cached) {
    return JSON.parse(cached);
  }

  // APIを呼び出し
  const result = await fetchFromWhereParcel(carrier, trackingNumber);

  // ステータスに応じた期間でキャッシュ
  const ttl = getCacheDuration(result.status);
  await cache.set(cacheKey, JSON.stringify(result), ttl);

  return result;
}

効果: 配達完了の荷物へのリクエストがほぼゼロに。全体で 40〜60%のリクエスト削減 が見込めます。

戦略3:Exponential Backoffでリトライする

429 Too Many Requests を受け取った場合は、即座にリトライせず、待機時間を指数的に増やしながらリトライしましょう。

/**
 * Exponential Backoffでリトライするフェッチ関数
 */
async function fetchWithRetry(url, options, maxRetries = 5) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);

      // 429の場合はリトライ
      if (response.status === 429) {
        if (attempt === maxRetries) {
          throw new Error('レート制限超過:最大リトライ回数に到達しました');
        }

        // Retry-Afterヘッダーがあればそれに従う
        const retryAfter = response.headers.get('Retry-After');
        const waitTime = retryAfter
          ? parseInt(retryAfter) * 1000
          : Math.min(1000 * Math.pow(2, attempt) + Math.random() * 1000, 60000);

        console.log(`レート制限超過。${waitTime / 1000}秒後にリトライします(試行 ${attempt + 1}/${maxRetries})`);
        await sleep(waitTime);
        continue;
      }

      // 5xxエラーもリトライ
      if (response.status >= 500) {
        if (attempt === maxRetries) throw new Error(`サーバーエラー: ${response.status}`);
        await sleep(1000 * Math.pow(2, attempt));
        continue;
      }

      return response;
    } catch (error) {
      if (attempt === maxRetries) throw error;
      await sleep(1000 * Math.pow(2, attempt));
    }
  }
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

ポイントは、Retry-After ヘッダーが提供されている場合はそれに従い、提供されていない場合のみ Exponential Backoff を適用することです。また、ジッター(ランダムな揺らぎ)を加えることで、複数のクライアントが同時にリトライする「サンダリングハード問題」を防げます。

戦略4:ポーリングの代わりにWebhookを使用する

定期的にAPIをポーリングするのではなく、Webhookを使って配送ステータスの変更を受動的に受け取りましょう。

ポーリング方式の問題点

1,000件の荷物 × 10分ごとにポーリング × 24時間
= 144,000リクエスト/日

→ Businessプランの上限(100,000/日)を超過!

Webhook方式の優位性

1,000件の荷物 × 平均5回のステータス変更/荷物
= 約5,000イベント/日

→ 初回登録の1,000リクエスト + Webhookは無料受信
= 合計約1,000リクエスト/日
// ポーリング方式(非推奨)
setInterval(async () => {
  for (const parcel of activeParcels) {
    await fetchTracking(parcel);  // 毎回APIを消費
  }
}, 10 * 60 * 1000);  // 10分ごと

// Webhook方式(推奨)
app.post('/webhooks/tracking', (req, res) => {
  res.status(200).json({ received: true });
  processUpdate(req.body);  // APIリクエスト消費なし
});

効果: 144,000リクエスト → 約1,000リクエスト(99%削減

戦略5:アクティブな荷物の優先度を設定する

Webhookが利用できない場合やバックアップとしてポーリングを併用する場合、すべての荷物を同じ頻度でポーリングする必要はありません。出荷後の経過日数に応じてポーリング間隔を調整しましょう。

/**
 * 荷物の優先度に基づいてポーリング間隔を決定する
 */
function getPollingInterval(parcel) {
  const daysSinceShipment = getDaysSince(parcel.shippedAt);
  const status = parcel.lastKnownStatus;

  // 配達完了・返送済みの荷物はポーリング不要
  if (['delivered', 'returned', 'expired'].includes(status)) {
    return null;  // ポーリングしない
  }

  // 配達中の荷物は最優先
  if (status === 'out_for_delivery') {
    return 5 * 60 * 1000;  // 5分ごと
  }

  // 出荷後の日数に応じて間隔を調整
  if (daysSinceShipment <= 2) {
    return 15 * 60 * 1000;   // 15分ごと(出荷直後は頻繁に更新)
  } else if (daysSinceShipment <= 5) {
    return 30 * 60 * 1000;   // 30分ごと(配送中期)
  } else if (daysSinceShipment <= 10) {
    return 60 * 60 * 1000;   // 1時間ごと(長距離・国際配送)
  } else {
    return 4 * 60 * 60 * 1000; // 4時間ごと(長期間未配達)
  }
}

/**
 * 優先度に基づいてポーリングを実行するスケジューラー
 */
async function smartPollingScheduler() {
  const parcels = await getActiveParcels();

  for (const parcel of parcels) {
    const interval = getPollingInterval(parcel);

    // ポーリング不要な荷物はスキップ
    if (interval === null) continue;

    // 前回のポーリングからの経過時間を確認
    const timeSinceLastPoll = Date.now() - parcel.lastPolledAt;
    if (timeSinceLastPoll < interval) continue;

    // ポーリング実行
    await fetchAndUpdateTracking(parcel);
  }
}

この戦略により、配達完了の荷物への無駄なリクエストを完全に排除し、配達が近い荷物の情報を優先的に更新できます。

API使用量の監視

上記の戦略を実装した上で、API使用量を継続的に監視することが重要です。

/**
 * API使用量を監視するミドルウェア
 */
class APIUsageMonitor {
  constructor() {
    this.requestCount = 0;
    this.rateLimitHits = 0;
    this.lastReset = Date.now();
  }

  /**
   * レスポンスヘッダーからレート制限情報を抽出・記録する
   */
  trackResponse(response) {
    this.requestCount++;

    const remaining = parseInt(response.headers.get('X-RateLimit-Remaining') || '0');
    const limit = parseInt(response.headers.get('X-RateLimit-Limit') || '0');

    if (response.status === 429) {
      this.rateLimitHits++;
    }

    // 残りリクエスト数が20%を切ったら警告
    if (limit > 0 && remaining / limit < 0.2) {
      console.warn(`API残量警告:残り${remaining}/${limit}(${Math.round(remaining / limit * 100)}%)`);
    }

    // 残りが0に近づいたらスロットリング
    if (remaining <= 5) {
      console.error('APIリクエスト残量がほぼゼロです。リクエストを一時停止してください。');
    }
  }

  /**
   * 使用量レポートを出力する
   */
  getReport() {
    const elapsed = (Date.now() - this.lastReset) / 1000 / 60;
    return {
      totalRequests: this.requestCount,
      rateLimitHits: this.rateLimitHits,
      requestsPerMinute: Math.round(this.requestCount / elapsed),
      rateLimitHitRate: `${Math.round(this.rateLimitHits / this.requestCount * 100)}%`
    };
  }
}

戦略別の削減効果まとめ

各戦略の期待削減効果を一覧にまとめます。

戦略説明期待削減率実装難易度
バッチ追跡複数荷物を1リクエストにまとめる90〜98%
スマートキャッシングステータスに応じたキャッシュ期間40〜60%
Exponential Backoff429エラー時の適切なリトライ直接削減ではなく安定性向上
Webhook活用ポーリングをWebhookに置き換える95〜99%
優先度ベースポーリング荷物の状態に応じたポーリング頻度50〜70%

これらの戦略を組み合わせることで、APIリクエスト数を 元の1/10〜1/100 に削減できます。特にバッチ追跡とWebhookの組み合わせが最も効果的で、大半のユースケースではこの2つだけで十分です。

まとめ

大量の荷物を追跡する際は、「いかにAPIを呼ばないか」がカギとなります。バッチ処理で呼び出し回数を減らし、キャッシュで不要な再取得を防ぎ、Webhookでポーリング自体を不要にする。これらの戦略を組み合わせれば、Freeプランでも数百件の荷物を効率的に管理できますし、Businessプランなら数万件の荷物にも余裕を持って対応できます。

各戦略の詳細な実装方法については、APIドキュメントをご参照ください。