レート制限とクォータ制限
サーバーサイドのレート制限とクォータ制限で、バックエンド関数を悪用から保護します。
レート制限とクォータ制限を使う理由
バックエンド関数はインターネットに公開されています。どのクライアントからでも呼び出せるため、制限がないと、単一のユーザーやボットがサービスを圧倒したり、API クォータを使い切ったり、想定外のコストを発生させたりする可能性があります。
// 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 デコレーターを使うと、あらゆる executable、webhook、または OpenAPI 関数に対して、レート制限(1 秒あたりのリクエスト数)とクォータ制限(一定期間あたりの総呼び出し回数)を定義できます。制限は関数本体が実行される前にサーバーサイドで適用されます。
いつ使うか
| シナリオ | 推奨 |
|---|---|
| バースト的な悪用を防ぐ(例:連射の API 呼び出し) | レート制限を使用 |
| 請求期間内の総利用量に上限を設ける | クォータ制限を使用 |
| 高コストな外部 API を保護する | 両方を併用 |
| 未認証アクセスを制限する | global または ip スコープの制限を使用 |
| ユーザー単位で公平な利用 | authentication と組み合わせて user スコープの制限を使用 |
仕組み
- バックエンド関数に
@limitsデコレーターを追加します - バックエンドをデプロイすると、Squid が制限を登録します
- 各呼び出し時に、Squid は関数を実行する前にすべての制限をチェックします
- いずれかの制限を超過している場合、呼び出しはエラーで拒否され、関数本体は実行されません
- レート制限のバケットは徐々に補充され、クォータ制限は固定期間で更新されます
クイックスタート
前提条件
squid init-backendで初期化された Squid backend プロジェクト@squidcloud/backendパッケージがインストールされていること
ステップ 1: @limits デコレーターを追加する
limits を import し、関数に適用します。
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 は、グローバルに 1 秒あたり 5 回、グローバルに 月あたり 200 回に制限されます。
ステップ 2: バックエンドを起動またはデプロイする
ローカル開発では、Squid CLI を使ってバックエンドをローカル実行します。
squid start
クラウドにデプロイするには、deploying your backend を参照してください。
ステップ 3: 制限の挙動を確認する
クライアントから関数を呼び出します。制限を超過すると、それ以降の呼び出しは拒否されます。
try {
const result = await squid.executeFunction('greet', 'World');
console.log(result);
} catch (error) {
console.error('Limit exceeded:', error.message);
}
@limits デコレーター
@limits デコレーターは、2 つの任意パラメータ rateLimit と quotaLimit を受け取ります。
rateLimit
rateLimit は次の 3 つの形式で定義できます。
- 関数を制限したい 1 秒あたりのクエリ数を表す number。これはデフォルトで
globalスコープになります。
@limits({ rateLimit: 5 })
- スコープをカスタマイズできる object。
scopeはuser、ip、globalのいずれかです。
@limits({ rateLimit: { value: 7, scope: 'user' } })
- 複数の制限を重ねがけできる object のリスト。
@limits({
rateLimit: [
{ value: 5, scope: 'user' },
{ value: 10, scope: 'ip' }
]
})
このリスト内のすべての制限は、各クエリごとに消費されます。最初に消費された制限が「超過」と判定した場合でも、例外がクライアントに返される前に他のすべての制限も消費されます。複数の制限が同じクエリを拒否する場合、最初に拒否したものがクライアントに返されます。
quotaLimit
quotaLimit も同じ 3 つの形式で定義できます。
- 関数をクエリできる総回数を表す number。これはデフォルトで
globalスコープ、更新期間(renewal period)はmonthlyになります。
@limits({ quotaLimit: 5 })
- スコープと更新期間をカスタマイズできる object。
@limits({ quotaLimit: { value: 7, scope: 'user', renewPeriod: 'annually' } })
注: scope と renewPeriod は任意です。指定しない場合でも、デフォルトの global と monthly が適用されます。
- 複数の制限を重ねがけできる object のリスト。
@limits({
quotaLimit: [
{ value: 7, scope: 'user', renewPeriod: 'monthly' },
{ value: 20, scope: 'user', renewPeriod: 'annually' }
]
})
レート制限とクォータ制限を両方使う
2 つのパラメータを併用して、レート制限とクォータ制限の両方を定義できます。
@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 回があるとします。
@limits({
quotaLimit: [
{ value: 5, renewPeriod: 'monthly' },
{ value: 10, renewPeriod: 'annually' }
]
})
月の最初の 1 週間で 5 回クエリが行われた場合、年次クォータに到達していなくても、その月の残り期間は追加のクエリができません。同様に、年の最初の 2 か月で毎月 5 回ずつ(合計 10 回)クエリが行われた場合、残りの月で月次クォータに到達していなくても、その年の残り期間は追加のクエリができません。
Enforcement
レート制限は常に評価され、クエリが拒否された場合でも消費されることがあります。これが何を意味するか、2 つの例で説明します。
レート制限に到達する場合
次の設定例を見てください。
@limits({ rateLimit: 5, quotaLimit: 20 })
予算の推移は次のとおりです。
| イベント | 残りレート予算 | 残りクォータ予算 | 結果 |
|---|---|---|---|
| 初期値 | 5 | 20 | |
| 5 回クエリを実行 | 0 | 15 | クエリ成功 |
| 6 回目のクエリを実行 | 0 | 15 | レート制限により拒否 |
レート制限が 6 回目のクエリを拒否し、クォータ制限は消費されません。
クォータ制限に到達する場合
一方、クォータ超過では、常にレート制限の消費が伴います。
次の設定例を見てください。
@limits({ rateLimit: 10, quotaLimit: 5 })
予算の推移は次のとおりです。
| イベント | 残りレート予算 | 残りクォータ予算 | 結果 |
|---|---|---|---|
| 初期値 | 10 | 5 | |
| 5 回クエリを実行 | 5 | 0 | クエリ成功 |
| 6 回目のクエリを実行 | 4 | 0 | クォータ制限により拒否 |
クォータ制限が 6 回目のクエリを拒否しますが、レート制限は消費され、予算は 4 まで減ります。
制限を超過した場合
制限を超えると、関数は状況に応じて「Rate limit on name exceeded」または「Quota on name exceeded」というメッセージを含む例外を返します。
name は、関数名・スコープ・(クォータ制限の場合)更新期間を含む文字列です。スコープが user または IP の場合は、ユーザー ID または IP アドレスも文字列に含まれます。
user/IP ベースの制限で user または IP が不明な場合
user/IP ベースの制限を定義した場合、何らかの理由でクライアントの user または IP が不明であれば、そのクライアントは他のすべての「不明」クライアントと同じ 1 つの unknown エンティティとしてバケット化されます。つまり、未ログインのユーザーは 1 人のユーザーとして扱われ、同じレート/クォータバケットを消費します。
たとえば、次の制限がある場合:
@limits({ rateLimit: { value: 7, scope: 'user' } })
ログイン済みユーザーはそれぞれ 1 秒あたり 7 回の独自バケットを持ちますが、不明なユーザーは全員で 1 秒あたり 7 回の単一バケットを共有します。
Atomicity
クエリがバッチとして送信され、バッチの途中で制限に達した場合、バッチ全体が拒否されます。これにより、部分的な変更が行われないことが保証されます。
補充(refill)と更新(renewal)
クォータの更新
未使用のクォータは次の期間へ繰り越されません。各クォータには更新期間が定義されており、その正確な期間は次のとおりです。
| 期間 | 長さ |
|---|---|
| hourly | 1 時間 |
| daily | 1 日 |
| weekly | 7 日 |
| monthly | 30 日 |
| quarterly | 90 日 |
| annually | 365 日 |
Squid は、次のうち早い方でクォータを更新します。
- 定期的: 毎時ちょうど(毎時 0 分)に、各クォータ制限が更新対象かどうかをチェックします。
- オンデマンド: クエリがクォータを超過したとき、その時点でクォータが更新対象であれば更新されます。
クォータ期間の開始時刻は、その特定のクォータ(関数、スコープ、更新期間、値のユニークな組み合わせ)がバックエンドのデプロイによって最初に導入された時刻です。
レート制限の補充
消費バケットは徐々に補充され、指定レートの最大 3 倍までのバーストを許容します。
Gradual refill の例: 制限 @limits({ rateLimit: 5 }) を定義し、クライアントが制限を超過した場合でも、次のクエリまで待つのは 1/5 秒(0.2 秒)で十分です。
制限の変更
新しいバックエンドをデプロイすることで、いつでも制限を変更できます。クォータの場合、特定の「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 し、適切に処理します。
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 ユーザーが単一バケットを共有している | authentication を必須にして、ユーザーごとにバケットを分ける |
| デプロイのたびに制限がリセットされる | 制限値を変更するとアクティブカウントがリセットされる | デプロイを跨いでカウントを維持したい場合は値を安定させる |
| 想定どおりにクォータが更新されない | 更新チェックは毎時、または次回の超過呼び出し時に行われる | 次の毎時チェックを待つか、もう 1 回呼び出してオンデマンド更新をトリガーする |
| バーストに対してレート制限が厳しすぎる | デフォルトのバケットは 3 倍バーストを許容するが、レート値が低すぎる可能性 | 想定されるバーストに合わせて rateLimit の値を上げる |
ベストプラクティス
適切なスコープを選ぶ
global: システム全体の保護に使用(例:外部 API への総負荷を制限)。すべての呼び出し元が同じバケットを共有します。ip: 認証がないがクライアント単位の制限が必要な場合に使用。公開エンドポイントに有効です。user: ユーザーごとの公平な利用に使用。効果を発揮するには authentication が必要で、そうでなければ未認証ユーザーが 1 つのバケットを共有します。
適切な値を設定する
- 外部 API の制限やサービス容量を基準にレート制限を決めます。
- 請求期間あたりの想定利用パターンに基づいてクォータ制限を設定します。
- まずは余裕のある制限から開始し、観測したトラフィックに基づいて絞り込みます。
レート制限とクォータ制限を重ねる
多層防御(defense in depth)のために両方を併用します。
- レート制限はバースト的な悪用を防ぎます(例:ボットが毎秒 100 リクエストを送る)
- クォータ制限は継続的な悪用を防ぎます(例:ユーザーが 1 か月で 10,000 リクエストを送る)
認証と組み合わせる
user スコープの制限を意味のあるものにするには、ユーザーが認証されている必要があります。そうでない場合、匿名ユーザーは 1 つのバケットを共有し、1 つの悪質なクライアントが全員分の制限を使い切ってしまう可能性があります。ユーザースコープの制限は常に認証チェックと組み合わせてください。
@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 プロキシ
この例は、外部 API へのリクエストをプロキシする現実的なサービスを示しています。レート制限とクォータ制限の両方、認証、エラーハンドリングを含みます。
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 は課金プランに関連するクォータを維持しており、定義した制限で拒否されたかどうかにかかわらず、すべてのクエリを課金プラン上のクォータとしてカウントします。
たとえば、次のクォータ制限を定義したとします。
@limits({ quotaLimit: 5 })
そして 8 回クエリを行った場合、最初の 5 回は成功し、最後の 3 回は拒否されます。課金されるのは成功した 5 回のみですが、Squid はアカウントのクォータに対して 8 回のクエリをカウントします。
Squid のクォータと課金についての詳細は、Quotas and limits documentation を参照してください。