Elido
1分で読了エンジニアリング

URL短縮API: レート制限、リトライ、冪等性

URL短縮APIを本番環境で呼び出す方法。トークンバケット方式のレート制限、バックオフ付きリトライが必要なステータスコード、重複を防ぐ冪等性キーを解説。

Marius Voß
DevRel · edge infra
トークンバケットがAPIリクエストを計量し、バックオフ付きリトライループが動作し、冪等性キーが重複する作成リクエストを排除する様子。Elidoブランドカラーを使用

3つのエンドポイント、認証ヘッダー、JSONボディ。URL短縮APIはバックログの中でも最も簡単な統合の一つであり、クイックスタートで数分後には動作する短縮リンクが得られる。クイックスタートが省略しているのは、統合が量産環境で動き始めたときに起きるすべてのことだ。レートリミッターが押し返してくる、バッチ処理の途中でで一時的な 503 が発生する、同じメッセージを2回配信するジョブキューがある。これらを誤ると、重複リンク、失われた処理、そして事態を悪化させる 429 の嵐が生まれる。

この記事はAPIクイックスタートの本番環境向け補足だ。デモと信頼性の高い統合を分ける3つのメカニズムを扱う。レート制限とそれに合わせたペーシング、リトライすべきエラーとバックオフの方法、そしてリトライが2つ目のリンクを作らないようにする冪等性キーだ。例はElidoのAPIを使うが、パターンはよく設計されたリンク短縮APIなら同じだ。短縮リンクをコードで管理するインフラとして扱う場合、その広い文脈はTerraformとしての短縮リンクで説明している。

レート制限: トークンバケットと3つのヘッダー#

Elidoはワークスペース単位でスコープされたトークンバケットでAPIを計量する。公開されている持続レートはFreeが毎秒10リクエスト、Proが毎秒100、Businessが毎秒500、Enterpriseは交渉による上限だ。Proはバースト容量200を持ち、バケットが満杯なら200リクエストを一度に送信してから毎秒100の持続レートに戻ることができる。大半のリンク作成ジョブはバースト内に収まり、制限をまったく感じない。

自分がどの位置にいるかを推測する必要はない。すべてのレスポンスには3つのヘッダーが含まれる。

  • X-RateLimit-Limit - 現在の毎秒上限。
  • X-RateLimit-Remaining - 現在のウィンドウで残っているトークン数。
  • X-RateLimit-Reset - バケットが補充されるUnixタイムスタンプ。

行儀のよいクライアントは X-RateLimit-Remaining を読んでゼロになる前にペースを落とす。429 の壁に突撃してから事後に対応するのではなく、先読みしてペーシングすることでスループットをスムーズに保つ。すべての拒否後にリアクティブにリトライすることは往復を無駄にし、すべてのクライアントが同時にリトライすれば「サンダリングハード」を人工的に作り出す。

リクエストがトークンを消費しながらワークスペースのレートで補充されるトークンバケット。3つのレート制限レスポンスヘッダーが表示され、バケットが空になると429が返される

数千のリンクを作成する必要がある場合、単一作成エンドポイントをループするのではなく、POST /v1/links/bulk を使う。このエンドポイントは1リクエストに最大1000のリンクを受け付け、レート制限に対して1ユニットとしてカウントされる。1回の一括呼び出しで1000のリンクをトークン1つ分のコストで処理できる。1000回の単一呼び出しは1000トークンとバーストのほとんどを消費する。一括パスこそが、Google Sheetsインポートがリミッターに引っかかることなくキャンペーン分のリンクを移動する方法だ。

429 Too Many RequestsRFC 6585がこのためだけに予約したステータス)は、何秒待つべきかを伝える retry_after 値とともに返ってくる。これを尊重する。その数値はリミッターがトークンが利用可能になる正確なタイミングを教えてくれており、自分のバックオフが導き出すどんな推測よりも優れた情報だ。

リトライ: どのコードをリトライし、どうバックオフするか#

すべてのエラーがリトライに値するわけではなく、誤ったものをリトライすることで小さな障害が障害全体になる。レスポンスを2つの山に分類する。

リトライすべきもの(一時的なため): 429(速すぎた)、500502503504(サーバー側またはゲートウェイの障害で自然に回復する可能性がある)。リトライしてはいけないもの(同じリクエストが同様に失敗するため): 400(ペイロードが無効)、401(トークンが欠落または誤り)、403(トークンのスコープが不足)、404(リソースが存在しないか自分のものではない)、409(スラッグの競合または古いバージョンの編集)。1つ目の山は「待って再試行」、2つ目の山は「コードまたは入力を修正」だ。タイトなループで 400 をリトライするのは、バグを自分自身へのサービス拒否攻撃に変えることに等しい。

リトライ可能なコードに対して重要なアルゴリズムは、ジッター付き指数バックオフだ。単純な指数バックオフ(試行のたびに待機時間を2倍にする)でも、同じ瞬間に失敗したクライアント全員が同じタイミングでリトライするため、クライアントが同期してしまう。ランダム性を加えることで分散させる。AWSの指数バックオフとジッターに関するブログ記事が標準的なリファレンスであり、ジッター付きバージョンが競合を劇的に削減する理由を示している。TypeScriptのコンパクトな実装:

const RETRYABLE = new Set([429, 500, 502, 503, 504]);

async function withRetry<T>(
  call: () => Promise<Response>,
  max = 5,
): Promise<Response> {
  let attempt = 0;
  while (true) {
    const res = await call();
    if (res.ok || !RETRYABLE.has(res.status) || attempt >= max) return res;

    // Honor server guidance first; otherwise back off exponentially with full jitter.
    const retryAfter = Number(res.headers.get("retry-after"));
    const base =
      Number.isFinite(retryAfter) && retryAfter > 0
        ? retryAfter * 1000
        : Math.min(1000 * 2 ** attempt, 20_000);
    const wait = Math.random() * base; // full jitter
    await new Promise((r) => setTimeout(r, wait));
    attempt++;
  }
}

これを危険ではなく安全にする3つの要素がある。試行回数に上限を設けているため、永続的な障害は無限ループするのではなく明確に失敗する。サーバーが Retry-After を送信した場合はそれを尊重し、送信しない場合のみ計算されたバックオフにフォールバックする。そしてジッターを使うため、同じ瞬間に問題から回復するワーカーの集団がロックステップで殺到しない。公式SDKはこれと同じポリシーをデフォルトで実装している。@elido/sdkelido-python、Goクライアントはまさにその5つの一時的なコードをジッター付きバックオフでリトライする。これが手書きHTTPクライアントよりSDKに手を伸ばす主な理由だ。

リトライを次のセクションに結びつけるルールが一つある。作成のリトライは、その作成が冪等である場合にのみ安全だ。そうでなければ、リトライのたびに2つ目のリンクが作られるリスクがある。

冪等性: 重複リンクを作らない方法#

典型的な障害はこのような形だ。ワーカーが短縮リンクを作成し、リンクは作成されたが、200 が戻ってこない。戻りの途中で接続が切れた。ワーカーはタイムアウトを見て失敗と判断し、リトライする。これで1つのキャンペーンに2つのリンクができた。大規模になると、ダッシュボードが /foo/foo-1/foo-2 で埋まり、重複が下流のすべてのレポートを歪める。

冪等性キーはそのギャップを埋める。ミューテーションリクエストに Idempotency-Key ヘッダーを付与する(最大255文字の任意の文字列)と、サーバーはそれに対してレスポンスを保存する。同じキーを再度提示すると、操作が2回実行される代わりに元のレスポンス(ステータスコードとボディ)が返ってくる。このパターンはStripeが冪等なリクエストのために文書化しているものと同じで、信頼性の低いネットワークでの書き込みを安全にする標準的な方法だ。

成否を分ける詳細は、キーをどこから取得するかだ。試行ごとにランダムなキーを生成してはいけない。そうすると各リトライが新しい操作に見えてしまい、目的を果たせない。安定したビジネス識別子から導出して、同じ論理アクションが常に同じキーを持つようにする:

const link = await elido.links.create(
  { destinationUrl: order.landingUrl },
  { idempotencyKey: `order-${order.id}-link` },
);

これで同じジョブのリトライが再び order-12345-link を持ち、保存されたレスポンスにヒットし、すでに存在するリンクを返す。キューが何回再配信しても、注文1つにつき正確に1つのリンクができる。これにより、上記のバックオフループと作成を安全に組み合わせることができる。リトライと冪等性キーは同じ保証の2つの半分だ。

at-least-onceジョブキューが同じ冪等性キーを持つ2つの作成リクエストを送信する。サーバーは最初のレスポンスを保存し、2回目にはそれを返すため、リンクは正確に1つだけ作成される

覚えておくべき2つの境界がある。キーはワークスペース単位でスコープされる。2つのワークスペースで同じキーを使うと2つのリンクが作成される。これはマルチテナントAPIとして正しい動作だが、キーがグローバルだと思っているチームには驚きになる。そしてキャッシュは永遠ではない。Elidoでは (workspace, key) をキーとして24時間保持される。ウィンドウ内のリトライは重複排除されるが、詰まっていたジョブがようやく排出されて3日後にリトライされると、新しいリンクが作成される。複数日にわたるバッチでは、キーだけに頼らない。最初の成功時に返されたリンクIDを永続化し、作成を再実行する前に確認する。IETFはこのヘッダーをIdempotency-Keyドラフトで標準化しており、24時間ウィンドウの注意事項もそこで言及されている。

今日APIの統合を組もうとしており、自分のリトライに耐えられるようにしたいなら、無料ワークスペースで始めて、サービスアカウントトークンを生成し、重複が出てから後付けするのではなく最初の作成から冪等性キーを付与しよう。

まとめると#

本番環境レベルの作成呼び出しは、3つのメカニズムを積み重ねたものだ。レート制限ヘッダーに合わせてペーシングし、429 をめったに受け取らないようにする。一時的なコードのみをリトライし Retry-After を尊重するジッター付きバックオフで呼び出しをラップする。ビジネスIDから導出した冪等性キーを持ち歩き、リトライを安全にする。公式SDKを使えば最初の2つは無料で手に入り、キーだけを自分で用意すればよい:

import { Elido, ElidoRateLimitError } from "@elido/sdk";

const elido = new Elido({ token: process.env.ELIDO_TOKEN! });

export async function shortenForOrder(order: Order) {
  try {
    return await elido.links.create(
      { destinationUrl: order.landingUrl, tags: [`order:${order.id}`] },
      { idempotencyKey: `order-${order.id}-link` },
    );
  } catch (err) {
    if (err instanceof ElidoRateLimitError) {
      // SDK already retried with backoff; we are still limited. Defer the job.
      throw new RetryableJobError(err.retryAfter);
    }
    throw err; // non-retryable: surface it
  }
}

これは特殊な手法ではない。書き込み量の多いAPIが当然に持つべき規律を短縮リンクに適用したものだ。見返りは、負荷がかかっても正しく動作し、リンクインベントリを静かに壊さない統合だ。同じAPIの読み取り側(リミッターを叩かずにクリックデータを引き出す)については、トレードオフをクリックトラッキングのWebhooksとポーリングで扱っており、エンドポイントの全体像はAPIとSDKsページと開発者向けソリューションの概要で確認できる。

ブログ関連記事#

Elidoを試す

URLを貼り付けて短縮リンクを取得

登録不要。リンクは30日間有効。永久に保存するには登録してください。

Free、登録不要 · 1日あたり2件

Elidoを試す

EUホスティングのURL短縮サービス。カスタムドメイン、詳細な分析、オープンAPI付き。無料プラン - クレジットカード不要。

タグ
url shortener api rate limits
api idempotency key
retry with exponential backoff
429 too many requests
link shortener api
idempotent requests

続きを読む