レート制限とクォータ制限
サーバー側のレート制限とクォータ制限で、バックエンド関数を不正利用から保護します。
レート制限とクォータ制限を使う理由
バックエンド関数はインターネットに公開されています。どんなクライアントでも呼び出せてしまい、制限がなければ、単一のユーザーやボットがサービスを圧迫し、APIクォータを使い切ったり、想定外のコストを発生させたりする可能性があります。
- TypeScript
- Python
// 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>);
}
# Without limits: any client can call this as fast as they want
@executable()
async def call_external_api(self, query: str) -> dict:
api_key = self.secrets['API_KEY']
async with httpx.AsyncClient() as client:
resp = await client.get(
'https://api.example.com',
params={'q': query},
headers={'Authorization': f'Bearer {api_key}'},
)
return resp.json()
# 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 def call_external_api(self, query: str) -> dict:
api_key = self.secrets['API_KEY']
async with httpx.AsyncClient() as client:
resp = await client.get(
'https://api.example.com',
params={'q': query},
headers={'Authorization': f'Bearer {api_key}'},
)
return resp.json()
デコレーターを1つ付けるだけで、どのクライアントも回避できないサーバー側の強制(enforcement)が得られます。
概要
@limits デコレーターを使うと、任意の executable、webhook、または OpenAPI 関数に、レート制限(秒あたりのリクエスト数)とクォータ制限(一定期間あたりの総呼び出し回数)を定義できます。制限は、関数本体が実行される前にサーバー側で強制されます。
いつ使うべきか
| シナリオ | 推奨 |
|---|---|
| バースト的な不正利用の防止(例: 連射のAPI呼び出し) | レート制限を使用 |
| 課金期間内の総使用量に上限を設ける | クォータ制限を使用 |
| 高コストな外部APIを保護する | 両方を併用 |
| 未認証アクセスを制限する | global または ip スコープの制限を使用 |
| ユーザーごとの公平な利用 | user スコープの制限を authentication と併用 |
仕組み
- バックエンド関数に
@limitsデコレーターを追加します - バックエンドをデプロイすると、Squid が制限を登録します
- 各呼び出し時に、Squid は関数を実行する前にすべての制限をチェックします
- いずれかの制限を超えている場合、呼び出しはエラーで拒否され、関数本体は実行されません
- レート制限のバケットは徐々に補充され、クォータ制限は固定の期間で更新されます
クイックスタート
前提条件
- TypeScript
- Python
squid initで初期化した Squid backend プロジェクト@squidcloud/backendパッケージがNPMからインストール済み
squid initで初期化した Squid backend プロジェクトsquidcloud-backendパッケージがPyPIからインストール済み
Step 1: @limits デコレーターを追加する
limits を import し、関数に適用します:
- TypeScript
- Python
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}!`;
}
}
from squidcloud_backend import SquidService, executable, limits
class ExampleService(SquidService):
@limits({'rateLimit': 5, 'quotaLimit': 200})
@executable()
async def greet(self, name: str) -> str:
return f"Hello, {name}!"
これにより greet は、グローバルに 秒あたり5回、グローバルに 月あたり200回 に制限されます。
Step 2: バックエンドを起動またはデプロイする
ローカル開発では、Squid CLI を使ってバックエンドをローカルで実行します:
squid start
クラウドにデプロイする場合は、deploying your backend を参照してください。
Step 3: 制限の挙動を確認する
クライアントから関数を呼び出します。制限を超えると、それ以降の呼び出しは拒否されます:
- TypeScript
- Python
try {
const result = await squid.executeFunction('greet', 'World');
console.log(result);
} catch (error) {
console.error('Limit exceeded:', error.message);
}
try:
result = await squid.execute_function('greet', 'World')
print(result)
except Exception as error:
print(f'Limit exceeded: {error}')
@limits デコレーター
@limits デコレーターは、任意の2つのパラメータ rateLimit と quotaLimit を受け取ります。
rateLimit
rateLimit は次の3つの形式で定義できます:
- 関数を制限したい秒あたりのクエリ数を表す number。これは
globalスコープがデフォルトです:
- TypeScript
- Python
@limits({ rateLimit: 5 })
@limits({'rateLimit': 5})
- スコープのカスタマイズを可能にする object。
scopeパラメータはuser、ip、globalのいずれかです:
- TypeScript
- Python
@limits({ rateLimit: { value: 7, scope: 'user' } })
@limits({'rateLimit': {'value': 7, 'scope': 'user'}})
- 複数の制限を積み重ねられる object の list:
- TypeScript
- Python
@limits({
rateLimit: [
{ value: 5, scope: 'user' },
{ value: 10, scope: 'ip' }
]
})
@limits({
'rateLimit': [
{'value': 5, 'scope': 'user'},
{'value': 10, 'scope': 'ip'},
]
})
この list にある制限は、各クエリごとにすべて消費されます。最初に消費した制限が超過を示した場合でも、例外がクライアントに返される前に、他のすべての制限も消費されます。複数の制限が同じクエリを拒否している場合、最初に拒否したものがクライアントに返されます。
quotaLimit
quotaLimit も同じ3つの形式で定義できます:
- 関数をクエリできる総回数を表す number。これは
globalスコープとmonthlyの更新期間がデフォルトです:
- TypeScript
- Python
@limits({ quotaLimit: 5 })
@limits({'quotaLimit': 5})
- スコープと更新期間のカスタマイズを可能にする object:
- TypeScript
- Python
@limits({ quotaLimit: { value: 7, scope: 'user', renewPeriod: 'annually' } })
@limits({'quotaLimit': {'value': 7, 'scope': 'user', 'renewPeriod': 'annually'}})
注: scope と renewPeriod は任意で、指定しない場合でも global と monthly のデフォルトが適用されます。
- 複数の制限を積み重ねられる object の list:
- TypeScript
- Python
@limits({
quotaLimit: [
{ value: 7, scope: 'user', renewPeriod: 'monthly' },
{ value: 20, scope: 'user', renewPeriod: 'annually' }
]
})
@limits({
'quotaLimit': [
{'value': 7, 'scope': 'user', 'renewPeriod': 'monthly'},
{'value': 20, 'scope': 'user', 'renewPeriod': 'annually'},
]
})
レート制限とクォータ制限を両方使う
2つのパラメータを併用して、レート制限とクォータ制限の両方を定義できます:
- TypeScript
- Python
@limits({
rateLimit: [
{ value: 5, scope: 'user' },
{ value: 10, scope: 'ip' }
],
quotaLimit: [
{ value: 7, scope: 'user', renewPeriod: 'monthly' },
{ value: 20, scope: 'user', renewPeriod: 'annually' }
]
})
@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 クエリあるとします:
- TypeScript
- Python
@limits({
quotaLimit: [
{ value: 5, renewPeriod: 'monthly' },
{ value: 10, renewPeriod: 'annually' }
]
})
@limits({
'quotaLimit': [
{'value': 5, 'renewPeriod': 'monthly'},
{'value': 10, 'renewPeriod': 'annually'},
]
})
月の最初の週に 5 クエリが行われた場合、年次クォータに到達していなくても、その月の残り期間はクエリできません。同様に、年の最初の2か月で毎月 5 クエリずつ行い合計 10 クエリになると、残りの月では月次クォータにまだ余裕があっても、その年の残り期間はクエリできません。
Enforcement
レート制限は常に評価され、クエリが拒否される場合でも消費されることがあります。これが何を意味するか、2つの例で見てみましょう。
レート制限に達した場合
次の設定を考えます:
- TypeScript
- Python
@limits({ rateLimit: 5, quotaLimit: 20 })
@limits({'rateLimit': 5, 'quotaLimit': 20})
予算(budget)の推移は次のとおりです:
| イベント | 残りレート予算 | 残りクォータ予算 | 結果 |
|---|---|---|---|
| 初期値 | 5 | 20 | |
| 5クエリ実行 | 0 | 15 | クエリ成功 |
| 6回目のクエリ実行 | 0 | 15 | レート制限で拒否 |
レート制限が 6 回目のクエリを拒否し、クォータ制限は消費されません。
クォータ制限に達した場合
一方、クォータ超過は常にレート制限の消費を伴います。
次の設定を考えます:
- TypeScript
- Python
@limits({ rateLimit: 10, quotaLimit: 5 })
@limits({'rateLimit': 10, 'quotaLimit': 5})
予算(budget)の推移は次のとおりです:
| イベント | 残りレート予算 | 残りクォータ予算 | 結果 |
|---|---|---|---|
| 初期値 | 10 | 5 | |
| 5クエリ実行 | 5 | 0 | クエリ成功 |
| 6回目のクエリ実行 | 4 | 0 | クォータ制限で拒否 |
クォータ制限は 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 バケットを消費します。
たとえば、次の制限がある場合:
- TypeScript
- Python
@limits({ rateLimit: { value: 7, scope: 'user' } })
@limits({'rateLimit': {'value': 7, 'scope': 'user'}})
ログイン済みの各ユーザーは秒あたり 7 クエリの専用バケットを持ちますが、不明ユーザー(unknown users)は秒あたり 7 クエリの単一バケットを共有します。
Atomicity
クエリがバッチとして送信され、バッチの途中で制限に到達した場合、バッチ全体が拒否されます。これにより、部分的な変更が発生しないことが保証されます。
補充(refills)と更新(renewals)
クォータの更新
未使用のクォータは次の期間へ繰り越されません。各クォータには更新期間が定義されており、正確な期間は次のとおりです:
| Period | Duration |
|---|---|
| hourly | 1 hour |
| daily | 1 day |
| weekly | 7 days |
| monthly | 30 days |
| quarterly | 90 days |
| annually | 365 days |
Squid は、次の2つの方法で(どちらか早い方で)クォータを更新します:
- 定期的: 毎時ちょうど(各時間の0分)に、各 quota limit が更新対象かどうかチェックされます。
- オンデマンド: クエリがクォータを超過した場合でも、その時点でクォータが更新対象であれば更新されます。
クォータ期間の開始時刻は、その特定のクォータ(関数・スコープ・更新期間・値のユニークな組み合わせ)がバックエンドのデプロイで初めて導入された時刻です。
レート制限の補充
消費バケットは徐々に補充され、指定レートの最大 3 倍までのバーストを許容します。
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 し、適切に処理します:
- TypeScript
- Python
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);
}
}
from squidcloud.http import SquidHttpError
try:
result = await squid.execute_function('call_external_api', query)
print(result)
except SquidHttpError as error:
if error.status_code == 429 or 'limit' in str(error):
print('Rate or quota limit exceeded. Try again later.')
# Show a user-friendly message or implement backoff
else:
print(f'Unexpected error: {error}')
トラブルシューティング
| 症状 | 可能性が高い原因 | 対処 |
|---|---|---|
| 予期せず 429 エラーが出る | user スコープ制限で unknown users が単一バケットを共有している | authentication を必須にして、各ユーザーが専用バケットを持つようにする |
| デプロイのたびに制限がリセットされる | 制限値の変更によりアクティブカウントがリセットされる | デプロイをまたいでカウントを維持したい場合は値を安定させる |
| 想定どおりにクォータが更新されない | 更新チェックは毎時、または次の超過呼び出し時に行われる | 次の毎時チェックまで待つ、または別の呼び出しを行ってオンデマンド更新をトリガーする |
| バーストに対してレート制限が厳しすぎる | デフォルトバケットは 3x バーストを許容するが、値が低すぎる可能性 | 想定されるバーストパターンに合わせて rateLimit の値を増やす |
ベストプラクティス
適切なスコープを選ぶ
global: システム全体の保護に使用します(例: 外部APIへの総負荷を制限)。すべての呼び出し元が同じバケットを共有します。ip: 認証がないがクライアント単位で制限したい場合に使用します。公開エンドポイントに有効です。user: ユーザーごとの公平な利用に使用します。有効にするには authentication が必要です。そうでない場合、未認証ユーザーは全員で1つのバケットを共有します。
適切な値を設定する
- 外部APIの制限、またはサービスの処理能力に基づいてレート制限を設定します。
- 課金期間あたりの想定利用パターンに基づいてクォータ制限を設定します。
- 最初は余裕のある制限から始め、観測されたトラフィックに基づいて絞り込みます。
レート制限とクォータ制限をレイヤー化する
多層防御として両方を併用します:
- レート制限 はバースト的な不正利用から保護します(例: ボットが秒間100リクエストを送る)
- クォータ制限 は継続的な不正利用から保護します(例: ユーザーが1か月で10,000リクエストを送る)
認証と組み合わせる
user スコープの制限を意味のあるものにするには、ユーザーが認証されている必要があります。そうでない場合、匿名ユーザーは単一のバケットを共有し、1つの悪質クライアントが全員分の制限を使い切ってしまいます。user スコープ制限は常に認証チェックと組み合わせてください:
- TypeScript
- Python
@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}`;
}
@limits({
'rateLimit': {'value': 10, 'scope': 'user'},
'quotaLimit': {'value': 500, 'scope': 'user', 'renewPeriod': 'monthly'},
})
@executable()
async def protected_action(self, data: str) -> str:
self.assert_is_authenticated()
# ...
return f"Processed: {data}"
バックエンド関数のセキュリティ強化については、using auth in the backend を参照してください。
コード例
レイヤー化された制限を備えた API proxy
この例では、外部APIへのリクエストをプロキシする現実的なサービスを示します。レート制限とクォータ制限の両方、認証、エラーハンドリングを備えています:
- TypeScript
- Python
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>;
}
}
import httpx
from squidcloud_backend import SquidService, executable, limits
class TranslationService(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 def translate(self, text: str, target_lang: str) -> dict:
self.assert_is_authenticated()
if not text or len(text) > 5000:
raise ValueError('Text must be between 1 and 5000 characters')
api_key = self.secrets['TRANSLATION_API_KEY']
async with httpx.AsyncClient() as client:
response = await client.post(
'https://api.translation.example.com/v1/translate',
headers={
'Authorization': f'Bearer {api_key}',
'Content-Type': 'application/json',
},
json={'text': text, 'target': target_lang},
)
if response.status_code != 200:
print(f'Translation API error: {response.status_code}')
raise ValueError('Translation service unavailable')
return response.json()
この設定は、次の4層の保護を提供します:
- ユーザー単位のレート制限(5/sec): 単一ユーザーがエンドポイントを過剰に叩くのを防ぐ
- グローバルレート制限(20/sec): 総スループットに上限を設け、外部APIを保護する
- ユーザー単位の日次クォータ(100/day): ユーザーごとの公平な利用上限
- グローバル月次クォータ(2000/month): 外部APIに対する予算保護
アカウントへの影響の理解
定義した制限の超過により関数呼び出しが拒否された場合、その呼び出しは 課金対象の使用量にはカウントされません。ただし、Squid は課金プランに関連するクォータを維持しており、定義した制限で拒否されたかどうかに関係なく、すべてのクエリを課金プランのクォータに対して カウントします。
たとえば、次のクォータ制限を定義したとします:
- TypeScript
- Python
@limits({ quotaLimit: 5 })
@limits({'quotaLimit': 5})
そして 8 回クエリを行うと、最初の 5 回は成功し、最後の 3 回は拒否されます。課金されるのは成功した 5 回のみですが、Squid はアカウントのクォータに対して 8 回のクエリをカウントします。
Squid のクォータと課金の詳細は、Quotas and limits documentation を参照してください。