Rate と Quota の制限
server-side の rate limit と quota limit により、バックエンド function を不正利用から保護します。
Rate and Quota Limiting を使用する理由
バックエンド function はインターネットに公開されています。任意の client が呼び出すことができ、制限がなければ、単一の user または bot が service に過大な負荷をかけたり、API quota を使い果たしたり、想定外の費用を発生させたりする可能性があります。
- 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 つの decorator で、どの client も bypass できない server-side enforcement を実現できます。
概要
@limits decorator を使用すると、任意の executable、webhook、または OpenAPI function に rate limit(1 秒あたりの request 数)および quota limit(期間あたりの合計 call 数)を定義できます。function body が実行される前に、server-side で limit が適用されます。
使用する場面
| シナリオ | 推奨 |
|---|---|
| burst による不正利用を防止する(例: 高頻度な API call) | rate limit を使用 |
| 請求期間内の合計使用量を制限する | quota limit を使用 |
| 高コストな external API を保護する | 両方を併用する |
| 未認証 access を制限する | global または ip scope の limit を使用 |
| user ごとに公平な利用量を設定する | authentication とともに user scope の limit を使用 |
仕組み
- backend function に
@limitsdecorator を追加します - backend の deploy 時に、Squid が limit を登録します
- call ごとに、Squid は function を実行する前にすべての limit を確認します
- いずれかの limit を超過した場合、call は error で拒否され、function body は実行されません
- rate limit bucket は徐々に refill され、quota limit は固定期間ごとに renew されます
クイックスタート
前提条件
- TypeScript
- Python
squid initで初期化された Squid backend project- NPM からインストールされた
@squidcloud/backendpackage
squid initで初期化された Squid backend project- PyPI からインストールされた
squidcloud-backendpackage
ステップ 1: @limits decorator を追加する
limits を import し、function に適用します。
- 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 は global に 1 秒あたり 5 call、1 か月あたり 200 call に制限されます。
ステップ 2: backend を開始または deploy する
ローカル開発では、Squid CLI を使用して backend をローカルで実行します。
squid start
cloud に deploy するには、backend の deployを参照してください。
ステップ 3: limit の動作を確認する
client から function を呼び出します。limit 超過後の subsequent call は拒否されます。
- 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 Decorator
@limits decorator は、rateLimit と quotaLimit の 2 つの optional parameter を受け取ります。
rateLimit
rateLimit は 3 つの形式で定義できます。
- function を制限する 1 秒あたりの query 数を表す数値。default では
globalscope になります。
- TypeScript
- Python
@limits({ rateLimit: 5 })
@limits({'rateLimit': 5})
- scope のカスタマイズを有効にするobject。
scopeparameter にはuser、ip、globalを指定できます。
- TypeScript
- Python
@limits({ rateLimit: { value: 7, scope: 'user' } })
@limits({'rateLimit': {'value': 7, 'scope': 'user'}})
- 複数の limit を stack できる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 のすべての limit は、query ごとに消費されます。最初に消費された limit が超過を通知した場合でも、exception が client に返される前にほかのすべての limit も消費されます。複数の limit が同じ query を拒否する場合、最初に拒否した limit が client に返されます。
quotaLimit
quotaLimit も同じ 3 つの形式で定義できます。
- function を query できる合計回数を表す数値。default では
globalscope およびmonthlyrenewal period になります。
- TypeScript
- Python
@limits({ quotaLimit: 5 })
@limits({'quotaLimit': 5})
- scope と renewal period のカスタマイズを有効にするobject。
- TypeScript
- Python
@limits({ quotaLimit: { value: 7, scope: 'user', renewPeriod: 'annually' } })
@limits({'quotaLimit': {'value': 7, 'scope': 'user', 'renewPeriod': 'annually'}})
注記: scope と renewPeriod は optional であり、指定しない場合も global と monthly の default が適用されます。
- 複数の limit を stack できる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'},
]
})
Rate limit と quota limit の併用
2 つの parameter を併用して、rate limit と quota limit の両方を定義できます。
- 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'},
],
})
これらの limit の評価方法については、enforcement section を参照してください。
Limit の理解
任意の数の limit を定義できます。すべての limit は query ごとに消費されます。いずれかの limit を超過すると、他のすべての limit より優先され、query は拒否されます。
たとえば、function に月あたり 5 query の quota と年あたり 10 query の quota があるとします。
- TypeScript
- Python
@limits({
quotaLimit: [
{ value: 5, renewPeriod: 'monthly' },
{ value: 10, renewPeriod: 'annually' }
]
})
@limits({
'quotaLimit': [
{'value': 5, 'renewPeriod': 'monthly'},
{'value': 10, 'renewPeriod': 'annually'},
]
})
月の最初の週に 5 query を実行した場合、annual quota に達していなくても、その月の残りの期間では query を実行できません。同様に、年の最初の 2 か月間に月あたり 5 query、合計 10 query を実行した場合、残りの月で monthly quota に達していなくても、その年の残りの期間では query を実行できません。
Enforcement
rate limit は常に評価され、query が拒否された場合でも消費されることがあります。これが何を意味するか、2 つの例で説明します。
Rate limit に達する場合
次の configuration を使用します。
- TypeScript
- Python
@limits({ rateLimit: 5, quotaLimit: 20 })
@limits({'rateLimit': 5, 'quotaLimit': 20})
budget の推移:
| Event | 残り Rate Budget | 残り Quota Budget | 結果 |
|---|---|---|---|
| 初期値 | 5 | 20 | |
| 5 query を実行 | 0 | 15 | Query は成功 |
| 6 回目の query を実行 | 0 | 15 | Rate limit により拒否 |
rate limit は 6 回目の query を拒否し、quota limit は消費されません。
Quota limit に達する場合
一方、quota の超過時には常に rate limit が消費されます。
次の configuration を使用します。
- TypeScript
- Python
@limits({ rateLimit: 10, quotaLimit: 5 })
@limits({'rateLimit': 10, 'quotaLimit': 5})
budget の推移:
| Event | 残り Rate Budget | 残り Quota Budget | 結果 |
|---|---|---|---|
| 初期値 | 10 | 5 | |
| 5 query を実行 | 5 | 0 | Query は成功 |
| 6 回目の query を実行 | 4 | 0 | Quota limit により拒否 |
quota limit は 6 回目の query を拒否しますが、rate limit は引き続き消費され、budget は 4 になります。
Limit を超過した場合
limit を超過した場合、function は適切に "Rate limit on \name` exceeded"または"Quota on `name` exceeded"` という message を含む exception を返します。
name は、function 名、scope、quota limit の場合は renew period を含む string です。scope が user または IP の場合、user ID または IP address も string に含まれます。
User または IP が不明な場合の User/IP-based limit
user/IP-based limit を定義する場合、何らかの理由で client の user または IP が不明であれば、その client は他のすべての unknown client とともに、単一の unknown entity として bucket 化されます。つまり、login していないすべての user は単一の user として扱われ、同じ rate/quota bucket を消費します。
たとえば、次の limit がある場合:
- TypeScript
- Python
@limits({ rateLimit: { value: 7, scope: 'user' } })
@limits({'rateLimit': {'value': 7, 'scope': 'user'}})
login 済み user はそれぞれ 1 秒あたり 7 query の専用 bucket を取得しますが、unknown user は全員で 1 秒あたり 7 query の単一 bucket を共有します。
Atomicity
query が batch として送信され、その batch 内の途中で limit に達した場合、batch 全体が拒否されます。これにより partial change が行われないことを保証します。
Refill と Renewal
Quota renewal
未使用 quota は次の period に carry-over されません。各 quota には renewal period が定義され、その正確な duration は以下のとおりです。
| Period | Duration |
|---|---|
| hourly | 1 時間 |
| daily | 1 日 |
| weekly | 7 日 |
| monthly | 30 日 |
| quarterly | 90 日 |
| annually | 365 日 |
Squid は、先に発生した方の方法で quota を renew します。
- 定期的: 毎時 0 分に、各 quota limit が renewal 対象かどうか確認されます。
- オンデマンド: query が quota を超過しても、その時点で quota が renewal 対象であれば、renew されます。
quota period の start time は、特定の quota(function、scope、renew period、value の unique な組み合わせ)が backend deployment で初めて導入された時刻です。
Rate limit refill
consumption bucket は徐々に refill され、指定 rate の最大 3 倍までの burst を許可します。
段階的な refill の例: @limits({ rateLimit: 5 }) を定義して client が limit を超過した場合、次の query を実行するまでに client が待つ必要があるのは 1/5 秒(0.2 秒)だけです。
Limit の変更
新しい backend を deploy することで、いつでも limit を変更できます。quota の場合、特定の「limit combo」(function、scope、renewPeriod の unique な組み合わせ)の limit value を変更すると、active count が reset されます。たとえば user が 10 call を実行し、limit を 20 から 15 に変更した場合、user はさらに 15 call を実行できます(5 ではありません)。新しい backend deployment で指定した「limit combo」に変更がない場合、active count はreset されません。
Error Handling
Error type
limit を超過すると、client は HTTP status code 429(Too Many Requests) の error を受け取ります。error message は、超過した limit を示します。
- Rate limit:
"Rate limit on <name> exceeded" - Quota limit:
"Quota on <name> exceeded"
<name> には function 名、scope、quota の場合は renewal period が含まれるため、どの limit に到達したかを正確に識別できます。
Client-side の処理
client で limit error を 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}')
Troubleshooting
| 症状 | 考えられる原因 | 解決方法 |
|---|---|---|
| 予期せず 429 error が発生する | unknown user が user scope limit の単一 bucket を共有している | 各 user が専用 bucket を取得できるよう、authenticationを要求する |
| deploy のたびに limit が reset される | limit value の変更により active count が reset される | deploy 間で count を維持するには値を変更しない |
| 想定どおりに quota が renew されない | renewal は毎時または次の超過 call 時に確認される | 次の毎時の確認まで待つか、別の call を実行して on-demand renewal を trigger する |
| burst に対して rate limit が厳しすぎる | default bucket は 3 倍の burst を許可するが、limit が低すぎる可能性がある | 想定される burst pattern に対応できるよう rate limit value を増やす |
ベストプラクティス
適切な scope を選ぶ
global: system-wide protection(例: external API への合計 load の制限)に使用します。すべての caller が同じ bucket を共有します。ip: client ごとの limit が必要で authentication がない場合に使用します。public endpoint に適しています。user: user ごとに公平な利用量を設定する場合に使用します。有効にするには authentication が必要です。そうでない場合、未認証 user は全員で 1 つの bucket を共有します。
適切な値を設定する
- rate limit は external API の limit または service の capacity に基づいて設定します。
- quota limit は billing period ごとの想定 usage pattern に基づいて設定します。
- まず余裕のある limit から始め、観測された traffic に基づいて厳しくします。
Rate limit と quota limit を重ねる
defense in depth のために、両方を併用します。
- Rate limit は burst による不正利用(例: bot が毎秒 100 request を送信すること)から保護します
- Quota limit は継続的な不正利用(例: user が 1 か月で 10,000 request を送信すること)から保護します
Authentication と組み合わせる
user scope limit を意味のあるものにするには、user が認証されている必要があります。そうでない場合、anonymous user は全員で 1 つの bucket を共有し、1 つの不正 client が全員の limit を使い果たす可能性があります。user scope limit は常に authentication check と組み合わせてください。
- 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}"
backend function の保護の詳細については、backend での auth の使用を参照してください。
コード例
Layered limit を備えた API proxy
この例では、rate limit と quota limit、authentication、error handling のすべてを使用して、external API への request を proxy する現実的な service を示します。
- 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()
この configuration では、4 層の保護を提供します。
- User ごとの rate limit(5/秒): 単一 user が endpoint に過度な request を送信することを防止します
- Global rate limit(20/秒): external API を保護するため、合計 throughput を制限します
- User ごとの daily quota(100/日): user ごとの fair usage limit
- Global monthly quota(2000/月): external API の budget 保護
Account への影響の理解
定義した limit の超過により function call が拒否された場合、billable usage には count されません。ただし、Squid は billing plan に関連する quota を維持しており、定義した limit により拒否されたかどうかにかかわらず、すべての query が billing plan に count されます。
たとえば、次の quota limit を定義したとします。
- TypeScript
- Python
@limits({ quotaLimit: 5 })
@limits({'quotaLimit': 5})
8 query を実行した場合、最初の 5 query は成功し、最後の 3 query は拒否されます。請求対象となるのは成功した 5 query のみですが、Squid は account の quota に対して 8 query を count します。
Squid の quota と billing の詳細については、Quotas and limits documentationを参照してください。