レート制限(API の日次上限と RateLimit.json)
プリザンター 1.5.8.1 には、リクエストの量を制限する仕組みが 2 つあります。
| 仕組み | 設定ファイル | 数える単位 | 期間 | カウンターの置き場所 | 対象 | ライセンス |
|---|---|---|---|---|---|---|
| API の日次上限 | Api.json の LimitPerSite | サイト(SiteId) | 1 日(日付が変わるとリセット) | DB(Sites テーブル) | API とサーバースクリプトのレコード操作 | 不要 |
| レートリミッター | RateLimit.json | ユーザー・IP アドレス・API キー | 秒〜分単位(ポリシーごと) | プロセスのメモリ | 画面操作・API・ログイン・フォームなどの HTTP リクエスト | RateLimit オプションが必要 |
どちらも既定では無効です。前者は LimitPerSite: 0、後者は Mode: "Off" になっています。MCP サーバーの制限は McpServer.json の RateLimit で別に設定します(プリザンターの MCPを参照)。
API の日次上限(LimitPerSite)
設定
App_Data/Parameters/Api.json の LimitPerSite が、サイトごとの 1 日あたりの上限件数です。0 は制限なしです(Api.json)。
{
"Version": 1.1,
"Enabled": true,
"PageSize": 200,
"LimitPerSite": 0,
"Compatibility_1_3_12": false
}テナントの契約設定(ContractSettings)に ApiLimitPerSite があれば、そちらが優先されます。実際に使われる上限値は ContractSettings.ApiLimit() が決めます(ContractSettings.cs#L41、#L139-L144)。
public int ApiLimit()
{
return (ApiLimitPerSite != null)
? (int)ApiLimitPerSite
: Parameters.Api.LimitPerSite;
}カウントの仕組み(WithinApiLimits)
判定は SiteModel.WithinApiLimits() です(SiteModel.cs#L10081-L10112)。
図を読み込み中…
- カウンターは
SitesテーブルのApiCount(当日の件数)とApiCountDate(最後にリセットした日付)で、サイトごとに独立しています。 - 「今日」はユーザーのタイムゾーンで判定します(
DateTime.Now.ToDateTime().ToLocal(context: context).Date)。 - 書き込みは
ApiCountとApiCountDateだけで、UpdatedTime・Updatorは変えません(addUpdatorParam: false、addUpdatedTimeParam: false。SiteModel.cs#L10117-L10131)。 - 日付が変わってリセットしたときは、リセット前後の件数を
SysLogsに記録します(メソッド名LogApiCountReset)。 - MCP からの呼び出し(
context.IsMcp)は数えません。
上限をわずかに超えることがある
WithinApiLimits() はロックを取らず、メモリ上の ApiCount を増やしてから DB に書き込みます。同じサイトへのリクエストが同時に来ると、同じ値を読んでそれぞれ書き込むため、上限を少し超えて許可されることがあります。
数える操作・数えない操作
WithinApiLimits() を呼んでいるのは ItemModel の API・サーバースクリプト用メソッドと、メール送信 API です(OutgoingMailUtilities.cs#L751)。
| 経路 | メソッド |
|---|---|
| API | GetByApi・CreateByApi・UpdateByApi・UpsertByApi・BulkUpsertByApi・DeleteByApi・BulkDeleteByApi・UpdateSiteSettingsByApi・ExportByApi・ImportByApi・CopySitePackageByApi |
| サーバースクリプト | GetByServerScript・GetSiteByServerScript・CreateByServerScript・UpdateByServerScript・UpsertByServerScript・DeleteByServerScript・BulkDeleteByServerScript |
| メール送信 | OutgoingMailUtilities の API 送信 |
画面(ブラウザ)からの作成・更新・削除(ItemModel.Create・Update・Delete・BulkDelete)では呼ばれないので、画面操作は数えません。サーバースクリプトの items 操作は数えるので、上限を設定するとサーバースクリプトが途中で失敗することがあります。
超過時の応答
API で上限を超えると HTTP 429 を返します(Error.Types.OverLimitApi → 429。ApiResponses.cs#L111-L113、#L208-L215)。メッセージは OverLimitApi の表示文字列です。
{
"Id": 12345,
"StatusCode": 429,
"Message": "ID: 12345 のサイトで利用できるAPIの制限(1000 件/日)を超えました。"
}上限が 0 以外のとき、成功した取得 API の応答には LimitPerDate(1 日の上限)と LimitRemaining(当日の残り件数)が入ります(IssueUtilities.cs#L3324)。
画面向けの応答を作る Messages.ResponseOverLimitApi() も定義されていますが、1.5.8.1 ではどこからも呼ばれていません(Messages.cs#L4501)。画面操作にもこの上限を掛ける改修案は画面操作へのレート制限の追加にまとめています。
レートリミッター(RateLimit.json)
ASP.NET Core の Microsoft.AspNetCore.RateLimiting を使った、HTTP リクエスト単位の制限です。コントローラーやアクションに [EnableRateLimiting("ポリシー名")] が付いていて、RateLimit.json でポリシーごとにアルゴリズムと上限を決めます。
有効になる条件
Startup.cs は起動時に次の条件を見て、レートリミッターを登録します(Startup.cs#L394-L429、#L656-L668)。
Parameters.AllowRateLimit()がtrue(ライセンスにRateLimitオプションがあるか、有効なトライアルライセンス。ライセンス判定を参照)。GlobalLimiterか、どれか 1 つのポリシーの実効モードがOnならAddRateLimiter()とUseRateLimiter()を登録します。LogOnlyがあれば観測用のミドルウェア(BodyRateLimitObserver)を登録します。/mcp配下には適用しません(MCP は別の仕組み)。
ミドルウェアを登録するかどうかは起動時に決まるので、Off から On・LogOnly に変えたときはアプリケーションを再起動します。ライセンスが無い環境では、RateLimit.json を書き換えても何も起きません。
既定の RateLimit.json
{
"Mode": "Off",
"ApplyPaths": ["/"],
"ExcludePaths": [
"/mcp", "/healthz", "/favicon.ico", "/Css", "/Scripts", "/fonts", "/images",
"/binaries", "/backgroundtasks", "/reminderschedules", "/api/backgroundtasks",
"/cspreport", "/errors", "/resources"
],
"KeyResolver": { "Order": ["User", "Ip"] },
"Exclusions": { "LoginIds": [] },
"GlobalLimiter": {
"Mode": "Inherit", "Algorithm": "TokenBucket", "Partition": "Auto",
"TokenLimit": 100, "TokensPerPeriod": 50, "ReplenishmentPeriodSeconds": 1, "QueueLimit": 0
},
"Policies": {
"General": { "Mode": "Inherit", "Algorithm": "TokenBucket", "Partition": "User",
"TokenLimit": 10, "TokensPerPeriod": 5, "ReplenishmentPeriodSeconds": 1, "QueueLimit": 0 },
"List": { "Mode": "Inherit", "Algorithm": "SlidingWindow", "Partition": "User",
"PermitLimit": 30, "WindowSeconds": 60, "SegmentsPerWindow": 6, "QueueLimit": 0 },
"Admin": { "Mode": "Inherit", "Algorithm": "FixedWindow", "Partition": "User",
"PermitLimit": 30, "WindowSeconds": 60, "QueueLimit": 0 },
"Heavy": { "Mode": "Inherit", "Algorithm": "Concurrency", "Partition": "User",
"PermitLimit": 1, "QueueLimit": 0 },
"ApiHeavy": { "Mode": "Inherit", "Algorithm": "Concurrency", "Partition": "ApiKey",
"PermitLimit": 1, "QueueLimit": 0 },
"Api": { "Mode": "Inherit", "Algorithm": "TokenBucket", "Partition": "Auto",
"TokenLimit": 30, "TokensPerPeriod": 10, "ReplenishmentPeriodSeconds": 1, "QueueLimit": 0 },
"AnonymousIp": { "Mode": "Inherit", "Algorithm": "FixedWindow", "Partition": "Ip",
"PermitLimit": 60, "WindowSeconds": 60, "QueueLimit": 0 },
"PublicForm": { "Mode": "Inherit", "Algorithm": "TokenBucket", "Partition": "Ip",
"TokenLimit": 5, "TokensPerPeriod": 2, "ReplenishmentPeriodSeconds": 1, "QueueLimit": 0 }
},
"RejectedResponse": {
"IncludeRetryAfter": true,
"LogRejected": true
}
}実際のファイルは 1 項目 1 行で書かれています(RateLimit.json)。値はここに挙げたものと同じです。
項目
| 項目 | 内容 |
|---|---|
Mode | 全体のモード。Off・LogOnly・On |
ApplyPaths | 対象にするパスの先頭(/ で始める)。空なら全パス |
ExcludePaths | 対象から外すパスの先頭。ApplyPaths より先に判定 |
KeyResolver.Order | Partition: "Auto" のときにキーを決める順番 |
Exclusions.LoginIds | 制限しないログイン ID(大文字小文字を区別しない) |
GlobalLimiter | ポリシーの有無に関係なく全リクエストに掛ける制限 |
Policies | 名前付きポリシー(下表)。名前は固定で、新しい名前を足しても使われない |
RejectedResponse.IncludeRetryAfter | 拒否時に Retry-After ヘッダーを付けるか |
RejectedResponse.LogRejected | 拒否したリクエストをログに書くか |
各ポリシー(と GlobalLimiter)の項目です。
| 項目 | 内容 |
|---|---|
Mode | Inherit(全体の Mode に従う)・Off・LogOnly・On |
Algorithm | TokenBucket・FixedWindow・SlidingWindow・Concurrency |
Partition | 数える単位。User・Ip・ApiKey・Auto |
TokenLimit・TokensPerPeriod・ReplenishmentPeriodSeconds | TokenBucket のバケット容量・補充数・補充間隔(秒) |
PermitLimit・WindowSeconds | FixedWindow・SlidingWindow の許可数と時間枠(秒)。Concurrency では同時実行数 |
SegmentsPerWindow | SlidingWindow の時間枠の分割数 |
QueueLimit | 上限に達したときに待たせる数。0 なら待たせずに拒否 |
設定値の検証は RateLimit.Normalize() が起動時に行います(RateLimit.cs)。全体の Mode が不正なら Off、ポリシーの Mode・Algorithm・Partition が不正だったり、アルゴリズムに必要な値が 0 以下だったりすると、そのポリシーだけ Off になります。/ で始まらないパスは無視されます。警告は [RateLimit] で始まるメッセージとして SysLogs に Warning で記録されます(Startup.cs#L78-L88)。
モードの使い分け
Off: 制限しない。LogOnly: 制限はせず、「Onだったら拒否していた」リクエストをRateLimitWouldHaveRejectedとしてログに書く。上限値を決める前の観測に使う。On: 上限を超えたリクエストを HTTP 429 で拒否する。
全ポリシーが Inherit なので、全体の Mode を変えるだけで一斉に切り替わります。特定のポリシーだけ有効にするときは、全体を Off にしてそのポリシーの Mode を On(または LogOnly)にします。
ポリシーと適用先
1.5.8.1 のコントローラーに付いているポリシーです。
| ポリシー | 既定のアルゴリズム・上限 | 単位 | 主な適用先 |
|---|---|---|---|
General | TokenBucket(容量 10、毎秒 5 補充) | User | 画面のレコード操作(ItemsController のクラス全体。作成・更新・削除など下の 2 つ以外) |
List | SlidingWindow(60 秒で 30 回、6 分割) | User | 一覧(Index)・クロス集計・横断検索・ドロップダウン検索・GridRows・ReloadRow・ゴミ箱一覧 |
Heavy | Concurrency(同時 1) | User | インポート・エクスポート・一括更新/移動/削除・物理削除・レコードの分割・サイトパッケージ・検索インデックス再構築・集計/計算式の同期 |
Admin | FixedWindow(60 秒で 30 回) | User | テナント・組織・グループ・ユーザー・管理者・システムログの管理画面 |
Api | TokenBucket(容量 30、毎秒 10 補充) | Auto | /api 配下の各 API コントローラーのクラス全体 |
ApiHeavy | Concurrency(同時 1) | ApiKey | API の一括削除・一括 Upsert・インポート・エクスポート・サイトパッケージ複製・集計の同期・検索インデックス再構築 |
AnonymousIp | FixedWindow(60 秒で 60 回) | Ip | ログイン画面・認証・ログイン時のパスワード変更・SSO の Challenge・パスキーの認証・デモ登録 |
PublicForm | TokenBucket(容量 5、毎秒 2 補充) | Ip | 公開フォームの送信(FormsController.Create)とフォームの添付ファイル・画像のアップロード/削除(FormBinariesController) |
たとえば一覧は ItemsController.cs#L25-L30、公開フォームは FormsController.cs#L129 です。GlobalLimiter は、どのポリシーが付いているかに関係なく全リクエストに重ねて掛かります。
- 認証済みユーザーの添付ファイルのアップロード・ダウンロード(
/binaries)はExcludePathsに入っているので制限されません。公開フォームのアップロード(/formbinaries)はPublicFormの対象です。
数える単位(パーティション)
キーの決め方は BodyRateLimitHelper.ResolvePartitionKey() です(BodyRateLimitHelper.cs#L44-L80)。
Partition | キー | キーが取れないとき |
|---|---|---|
User | ログイン ID(User.Identity.Name) | 制限しない(未認証のリクエストは素通り) |
Ip | 接続元 IP アドレス(Connection.RemoteIpAddress) | unknown |
ApiKey | Authorization: Bearer の値、なければフォームの parameters か JSON 本文の ApiKey | IP アドレス |
Auto | KeyResolver.Order の順(既定は User → Ip) | IP アドレス |
- IP アドレスは
RemoteIpAddressです。UseForwardedHeaders()でX-Forwarded-Forを処理しますが、信頼するのはSecurity.jsonのForwardedHeadersのKnownNetworks・KnownProxiesに入れたプロキシ(と ASP.NET Core 既定のループバック)からのものだけです(Startup.cs#L241-L262)。別ホストのリバースプロキシを登録していないと、全員がプロキシの IP でまとめて数えられます。 - API キーを本文から読むのは
Content-Lengthが 10 MB 以下のときだけです(BodyRateLimitHelper.cs#L97)。 - カウンターはプロセスのメモリにあります。Web サーバーを複数台並べると台数分だけ実効の上限が増え、再起動でリセットされます。
拒否時の応答
上限を超えると HTTP 429 を返します。本文はリクエストの種類で変わります(BodyRateLimitHelper.cs#L311-L364)。
| 条件 | 本文 |
|---|---|
パスが /api で始まる、またはポリシーが Api・ApiHeavy | API と同じ形の JSON({"Id":0,"StatusCode":429,"Message":"..."}) |
X-Requested-With: XMLHttpRequest(画面の Ajax) | ResponseCollection 形式の JSON。画面にはエラーメッセージとして出る |
Accept に application/json を含む | API と同じ形の JSON |
| それ以外 | 簡単な HTML(429 Too Many Requests) |
- メッセージは表示文字列
RateLimitExceeded(日本語は「リクエストが多すぎます。しばらくしてから再度お試しください。」)です。言語はクエリのLanguage、Accept-Languageの先頭、Service.jsonのDefaultLanguageの順で決めます。 IncludeRetryAfter: trueならRetry-Afterヘッダー(秒)を付けます。リミッターが待ち時間を返さないときは 60 秒です。
ログ
レート制限のログは NLog の ratelimitlogs(Warn 以上)と ratelimitmetrics(Info 以上)ロガーから、appsettings.json の ratelimitlogsfile ターゲット(logs/年/月/日/ratelimitlogs.json)に出ます(appsettings.json#L347-L351、#L473-L482)。NLog の設定はログ出力の拡張を参照してください。
| イベント | 出る条件 |
|---|---|
RateLimitRejected | On で拒否し、LogRejected: true のとき |
RateLimitWouldHaveRejected | LogOnly で、On なら拒否していたとき |
RateLimitMetricsSnapshot | On か LogOnly のポリシーがあるとき、60 秒ごとの集計 |
- ログにはポリシー名・アルゴリズム・パーティション・パス・メソッド・ログイン ID などが入ります。API キーはそのまま出さず、SHA-256 の先頭 4 バイト(
apikey:sha256:xxxxxxxx)にします。 - 同じポリシー・同じキーの詳細ログは 10 秒に 1 件までに間引き、全体でも 10 秒あたり 50 件を超える分は件数だけの要約になります(
BodyRateLimitLogThrottle)。
導入の手順(例)
- 全体の
ModeをLogOnlyにして再起動し、しばらく運用してratelimitlogs.jsonのRateLimitWouldHaveRejectedを見る。 - 正当な操作が引っかかるポリシーは上限を上げる。連携用のアカウントは
Exclusions.LoginIdsに入れる。 ModeをOnにして再起動する。
関連ページ
- プリザンターの MCP — MCP 側のレート制限(
McpServer.json) - ライセンス判定 —
RateLimitオプションとAllowRateLimit() - ログ出力の拡張(NLog)
- フォーム機能
- 画面操作へのレート制限の追加 — 改修案