メインコンテンツまでスキップ

レート制限とクォータ制限

サーバー側のレート制限とクォータ制限で、バックエンド関数を不正利用から保護します。

レート制限とクォータ制限を使う理由

バックエンド関数はインターネットに公開されています。どんなクライアントでも呼び出せてしまい、制限がなければ、単一のユーザーやボットがサービスを圧迫し、APIクォータを使い切ったり、想定外のコストを発生させたりする可能性があります。

Backend code
// Without limits: any client can call this as fast as they want
@executable()
async callExternalApi(query: string): Promise<ApiResult> {
const apiKey = this.secrets['API_KEY'];
return await fetch(`https://api.example.com?q=${query}`, {
headers: { Authorization: `Bearer ${apiKey}` },
}).then((r) => r.json() as Promise<ApiResult>);
}

// With limits: 10 requests/second per user, 1000/month per user
@limits({
rateLimit: { value: 10, scope: 'user' },
quotaLimit: { value: 1000, scope: 'user', renewPeriod: 'monthly' },
})
@executable()
async callExternalApi(query: string): Promise<ApiResult> {
const apiKey = this.secrets['API_KEY'];
return await fetch(`https://api.example.com?q=${query}`, {
headers: { Authorization: `Bearer ${apiKey}` },
}).then((r) => r.json() as Promise<ApiResult>);
}

デコレーターを1つ付けるだけで、どのクライアントも回避できないサーバー側の強制(enforcement)が得られます。

概要

@limits デコレーターを使うと、任意の executablewebhook、または OpenAPI 関数に、レート制限(秒あたりのリクエスト数)とクォータ制限(一定期間あたりの総呼び出し回数)を定義できます。制限は、関数本体が実行される前にサーバー側で強制されます。

いつ使うべきか

シナリオ推奨
バースト的な不正利用の防止(例: 連射のAPI呼び出し)レート制限を使用
課金期間内の総使用量に上限を設けるクォータ制限を使用
高コストな外部APIを保護する両方を併用
未認証アクセスを制限するglobal または ip スコープの制限を使用
ユーザーごとの公平な利用user スコープの制限を authentication と併用

仕組み

  1. バックエンド関数に @limits デコレーターを追加します
  2. バックエンドをデプロイすると、Squid が制限を登録します
  3. 各呼び出し時に、Squid は関数を実行する前にすべての制限をチェックします
  4. いずれかの制限を超えている場合、呼び出しはエラーで拒否され、関数本体は実行されません
  5. レート制限のバケットは徐々に補充され、クォータ制限は固定の期間で更新されます

クイックスタート

前提条件

  • squid init で初期化した Squid backend プロジェクト
  • @squidcloud/backend パッケージがNPMからインストール済み

Step 1: @limits デコレーターを追加する

limits を import し、関数に適用します:

Backend code
import { executable, limits, SquidService } from '@squidcloud/backend';

export class ExampleService extends SquidService {
@limits({ rateLimit: 5, quotaLimit: 200 })
@executable()
async greet(name: string): Promise<string> {
return `Hello, ${name}!`;
}
}

これにより greet は、グローバルに 秒あたり5回、グローバルに 月あたり200回 に制限されます。

Step 2: バックエンドを起動またはデプロイする

ローカル開発では、Squid CLI を使ってバックエンドをローカルで実行します:

squid start

クラウドにデプロイする場合は、deploying your backend を参照してください。

Step 3: 制限の挙動を確認する

クライアントから関数を呼び出します。制限を超えると、それ以降の呼び出しは拒否されます:

Client code
try {
const result = await squid.executeFunction('greet', 'World');
console.log(result);
} catch (error) {
console.error('Limit exceeded:', error.message);
}

@limits デコレーター

@limits デコレーターは、任意の2つのパラメータ rateLimitquotaLimit を受け取ります。

rateLimit

rateLimit は次の3つの形式で定義できます:

  • 関数を制限したい秒あたりのクエリ数を表す number。これは global スコープがデフォルトです:
Backend code
@limits({ rateLimit: 5 })
  • スコープのカスタマイズを可能にする objectscope パラメータは useripglobal のいずれかです:
Backend code
@limits({ rateLimit: { value: 7, scope: 'user' } })
  • 複数の制限を積み重ねられる object の list:
Backend code
@limits({
rateLimit: [
{ value: 5, scope: 'user' },
{ value: 10, scope: 'ip' }
]
})
Note

この list にある制限は、各クエリごとにすべて消費されます。最初に消費した制限が超過を示した場合でも、例外がクライアントに返される前に、他のすべての制限も消費されます。複数の制限が同じクエリを拒否している場合、最初に拒否したものがクライアントに返されます。

quotaLimit

quotaLimit も同じ3つの形式で定義できます:

  • 関数をクエリできる総回数を表す number。これは global スコープと monthly の更新期間がデフォルトです:
Backend code
@limits({ quotaLimit: 5 })
  • スコープと更新期間のカスタマイズを可能にする object:
Backend code
@limits({ quotaLimit: { value: 7, scope: 'user', renewPeriod: 'annually' } })

注: scoperenewPeriod は任意で、指定しない場合でも globalmonthly のデフォルトが適用されます。

  • 複数の制限を積み重ねられる object の list:
Backend code
@limits({
quotaLimit: [
{ value: 7, scope: 'user', renewPeriod: 'monthly' },
{ value: 20, scope: 'user', renewPeriod: 'annually' }
]
})

レート制限とクォータ制限を両方使う

2つのパラメータを併用して、レート制限とクォータ制限の両方を定義できます:

Backend code
@limits({
rateLimit: [
{ value: 5, scope: 'user' },
{ value: 10, scope: 'ip' }
],
quotaLimit: [
{ value: 7, scope: 'user', renewPeriod: 'monthly' },
{ value: 20, scope: 'user', renewPeriod: 'annually' }
]
})

これらの制限がどのように評価されるかについては、enforcement セクションを参照してください。

制限の理解

制限はいくつでも定義できます。すべての制限は各クエリごとに消費されます。いずれかの制限を超過すると、その制限が他のすべての制限よりも優先され、クエリは拒否されます。

たとえば、ある関数に月次クォータが 5 クエリ、年次クォータが 10 クエリあるとします:

Backend code
@limits({
quotaLimit: [
{ value: 5, renewPeriod: 'monthly' },
{ value: 10, renewPeriod: 'annually' }
]
})

月の最初の週に 5 クエリが行われた場合、年次クォータに到達していなくても、その月の残り期間はクエリできません。同様に、年の最初の2か月で毎月 5 クエリずつ行い合計 10 クエリになると、残りの月では月次クォータにまだ余裕があっても、その年の残り期間はクエリできません。

Enforcement

レート制限は常に評価され、クエリが拒否される場合でも消費されることがあります。これが何を意味するか、2つの例で見てみましょう。

レート制限に達した場合

次の設定を考えます:

Backend code
@limits({ rateLimit: 5, quotaLimit: 20 })

予算(budget)の推移は次のとおりです:

イベント残りレート予算残りクォータ予算結果
初期値520
5クエリ実行015クエリ成功
6回目のクエリ実行015レート制限で拒否

レート制限が 6 回目のクエリを拒否し、クォータ制限は消費されません。

クォータ制限に達した場合

一方、クォータ超過は常にレート制限の消費を伴います。

次の設定を考えます:

Backend code
@limits({ rateLimit: 10, quotaLimit: 5 })

予算(budget)の推移は次のとおりです:

イベント残りレート予算残りクォータ予算結果
初期値105
5クエリ実行50クエリ成功
6回目のクエリ実行40クォータ制限で拒否

クォータ制限は 6 回目のクエリを拒否しますが、レート制限は予算が 4 まで消費されます。

制限を超過したとき

制限を超えると、関数は適切に "Rate limit on \name` exceeded"または"Quota on `name` exceeded"` というメッセージの例外を返します。

name は、関数名、スコープ、(クォータ制限の場合は)更新期間を含む文字列です。スコープが user または IP の場合、ユーザーIDまたはIPアドレスも文字列に含まれます。

ユーザー/IP が不明な場合の user/ip ベース制限

user/IP ベースの制限を定義する際、何らかの理由でクライアントの user または IP が不明な場合、そのクライアントは他のすべての不明クライアントとまとめて、単一の unknown エンティティとしてバケット化されます。言い換えると、ログインしていないすべてのユーザーは単一ユーザーとして扱われ、同じ rate/quota バケットを消費します。

たとえば、次の制限がある場合:

Backend code
@limits({ rateLimit: { value: 7, scope: 'user' } })

ログイン済みの各ユーザーは秒あたり 7 クエリの専用バケットを持ちますが、不明ユーザー(unknown users)は秒あたり 7 クエリの単一バケットを共有します。

Atomicity

クエリがバッチとして送信され、バッチの途中で制限に到達した場合、バッチ全体が拒否されます。これにより、部分的な変更が発生しないことが保証されます。

補充(refills)と更新(renewals)

クォータの更新

未使用のクォータは次の期間へ繰り越されません。各クォータには更新期間が定義されており、正確な期間は次のとおりです:

PeriodDuration
hourly1 hour
daily1 day
weekly7 days
monthly30 days
quarterly90 days
annually365 days

Squid は、次の2つの方法で(どちらか早い方で)クォータを更新します:

  1. 定期的: 毎時ちょうど(各時間の0分)に、各 quota limit が更新対象かどうかチェックされます。
  2. オンデマンド: クエリがクォータを超過した場合でも、その時点でクォータが更新対象であれば更新されます。

クォータ期間の開始時刻は、その特定のクォータ(関数・スコープ・更新期間・値のユニークな組み合わせ)がバックエンドのデプロイで初めて導入された時刻です。

レート制限の補充

消費バケットは徐々に補充され、指定レートの最大 3 倍までのバーストを許容します。

Note

Gradual refill の例: @limits({ rateLimit: 5 }) を定義し、クライアントが制限を超えた場合でも、そのクライアントは 1/5 秒(0.2s)待つだけで次のクエリを実行できます。

制限の変更

新しいバックエンドをデプロイすることで、制限はいつでも変更できます。クォータについては、特定の「limit combo」(関数、スコープ、renewPeriod のユニークな組み合わせ)の制限値を変更すると、アクティブなカウントがリセットされます。たとえば、ユーザーが 10 回呼び出しており、制限が 20 から 15 に変更された場合、ユーザーは(5 回ではなく)さらに 15 回呼び出せます。新しいバックエンドのデプロイで特定の「limit combo」に変更がない場合、アクティブカウントはリセット されません

エラーハンドリング

エラー種別

制限を超過すると、クライアントは HTTP ステータスコード 429 (Too Many Requests) のエラーを受け取ります。エラーメッセージには、どの制限が超過されたかが示されます:

  • レート制限: "Rate limit on <name> exceeded"
  • クォータ制限: "Quota on <name> exceeded"

<name> には関数名、スコープ、(クォータの場合)更新期間が含まれるため、どの制限に到達したかを正確に特定できます。

クライアント側の対処

クライアントで制限エラーを catch し、適切に処理します:

Client code
try {
const result = await squid.executeFunction('callExternalApi', query);
console.log(result);
} catch (error: any) {
if (error?.statusCode === 429 || error?.message?.includes('limit')) {
console.warn('Rate or quota limit exceeded. Try again later.');
// Show a user-friendly message or implement backoff
} else {
console.error('Unexpected error:', error.message);
}
}

トラブルシューティング

症状可能性が高い原因対処
予期せず 429 エラーが出るuser スコープ制限で unknown users が単一バケットを共有しているauthentication を必須にして、各ユーザーが専用バケットを持つようにする
デプロイのたびに制限がリセットされる制限値の変更によりアクティブカウントがリセットされるデプロイをまたいでカウントを維持したい場合は値を安定させる
想定どおりにクォータが更新されない更新チェックは毎時、または次の超過呼び出し時に行われる次の毎時チェックまで待つ、または別の呼び出しを行ってオンデマンド更新をトリガーする
バーストに対してレート制限が厳しすぎるデフォルトバケットは 3x バーストを許容するが、値が低すぎる可能性想定されるバーストパターンに合わせて rateLimit の値を増やす

ベストプラクティス

適切なスコープを選ぶ

  • global: システム全体の保護に使用します(例: 外部APIへの総負荷を制限)。すべての呼び出し元が同じバケットを共有します。
  • ip: 認証がないがクライアント単位で制限したい場合に使用します。公開エンドポイントに有効です。
  • user: ユーザーごとの公平な利用に使用します。有効にするには authentication が必要です。そうでない場合、未認証ユーザーは全員で1つのバケットを共有します。

適切な値を設定する

  • 外部APIの制限、またはサービスの処理能力に基づいてレート制限を設定します。
  • 課金期間あたりの想定利用パターンに基づいてクォータ制限を設定します。
  • 最初は余裕のある制限から始め、観測されたトラフィックに基づいて絞り込みます。

レート制限とクォータ制限をレイヤー化する

多層防御として両方を併用します:

  • レート制限 はバースト的な不正利用から保護します(例: ボットが秒間100リクエストを送る)
  • クォータ制限 は継続的な不正利用から保護します(例: ユーザーが1か月で10,000リクエストを送る)

認証と組み合わせる

user スコープの制限を意味のあるものにするには、ユーザーが認証されている必要があります。そうでない場合、匿名ユーザーは単一のバケットを共有し、1つの悪質クライアントが全員分の制限を使い切ってしまいます。user スコープ制限は常に認証チェックと組み合わせてください:

Backend code
@limits({
rateLimit: { value: 10, scope: 'user' },
quotaLimit: { value: 500, scope: 'user', renewPeriod: 'monthly' },
})
@executable()
async protectedAction(data: string): Promise<string> {
this.assertIsAuthenticated();
// ...
return `Processed: ${data}`;
}

バックエンド関数のセキュリティ強化については、using auth in the backend を参照してください。

コード例

レイヤー化された制限を備えた API proxy

この例では、外部APIへのリクエストをプロキシする現実的なサービスを示します。レート制限とクォータ制限の両方、認証、エラーハンドリングを備えています:

Backend code
import { executable, limits, SquidService } from '@squidcloud/backend';

interface TranslationResult {
translatedText: string;
detectedLanguage: string;
}

export class TranslationService extends SquidService {
@limits({
rateLimit: [
{ value: 5, scope: 'user' },
{ value: 20, scope: 'global' },
],
quotaLimit: [
{ value: 100, scope: 'user', renewPeriod: 'daily' },
{ value: 2000, scope: 'global', renewPeriod: 'monthly' },
],
})
@executable()
async translate(text: string, targetLang: string): Promise<TranslationResult> {
this.assertIsAuthenticated();

if (!text || text.length > 5000) {
throw new Error('Text must be between 1 and 5000 characters');
}

const apiKey = this.secrets['TRANSLATION_API_KEY'];
const response = await fetch('https://api.translation.example.com/v1/translate', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ text, target: targetLang }),
});

if (!response.ok) {
console.error('Translation API error:', response.status);
throw new Error('Translation service unavailable');
}

return response.json() as Promise<TranslationResult>;
}
}

この設定は、次の4層の保護を提供します:

  • ユーザー単位のレート制限(5/sec): 単一ユーザーがエンドポイントを過剰に叩くのを防ぐ
  • グローバルレート制限(20/sec): 総スループットに上限を設け、外部APIを保護する
  • ユーザー単位の日次クォータ(100/day): ユーザーごとの公平な利用上限
  • グローバル月次クォータ(2000/month): 外部APIに対する予算保護

アカウントへの影響の理解

定義した制限の超過により関数呼び出しが拒否された場合、その呼び出しは 課金対象の使用量にはカウントされません。ただし、Squid は課金プランに関連するクォータを維持しており、定義した制限で拒否されたかどうかに関係なく、すべてのクエリを課金プランのクォータに対して カウントします

たとえば、次のクォータ制限を定義したとします:

Backend code
@limits({ quotaLimit: 5 })

そして 8 回クエリを行うと、最初の 5 回は成功し、最後の 3 回は拒否されます。課金されるのは成功した 5 回のみですが、Squid はアカウントのクォータに対して 8 回のクエリをカウントします。

Squid のクォータと課金の詳細は、Quotas and limits documentation を参照してください。