Skip to content

レート制限(API の日次上限と RateLimit.json) ​

第1版作成 最終更新 (日本時間)
確認バージョン1.5.8.1

プリザンター 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)。

json
{
    "Version": 1.1,
    "Enabled": true,
    "PageSize": 200,
    "LimitPerSite": 0,
    "Compatibility_1_3_12": false
}

テナントの契約設定(ContractSettings)に ApiLimitPerSite があれば、そちらが優先されます。実際に使われる上限値は ContractSettings.ApiLimit() が決めます(ContractSettings.cs#L41、#L139-L144)。

csharp
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)。

経路メソッド
APIGetByApi・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 の表示文字列です。

json
{
    "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 ​

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.OrderPartition: "Auto" のときにキーを決める順番
Exclusions.LoginIds制限しないログイン ID(大文字小文字を区別しない)
GlobalLimiterポリシーの有無に関係なく全リクエストに掛ける制限
Policies名前付きポリシー(下表)。名前は固定で、新しい名前を足しても使われない
RejectedResponse.IncludeRetryAfter拒否時に Retry-After ヘッダーを付けるか
RejectedResponse.LogRejected拒否したリクエストをログに書くか

各ポリシー(と GlobalLimiter)の項目です。

項目内容
ModeInherit(全体の Mode に従う)・Off・LogOnly・On
AlgorithmTokenBucket・FixedWindow・SlidingWindow・Concurrency
Partition数える単位。User・Ip・ApiKey・Auto
TokenLimit・TokensPerPeriod・ReplenishmentPeriodSecondsTokenBucket のバケット容量・補充数・補充間隔(秒)
PermitLimit・WindowSecondsFixedWindow・SlidingWindow の許可数と時間枠(秒)。Concurrency では同時実行数
SegmentsPerWindowSlidingWindow の時間枠の分割数
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 のコントローラーに付いているポリシーです。

ポリシー既定のアルゴリズム・上限単位主な適用先
GeneralTokenBucket(容量 10、毎秒 5 補充)User画面のレコード操作(ItemsController のクラス全体。作成・更新・削除など下の 2 つ以外)
ListSlidingWindow(60 秒で 30 回、6 分割)User一覧(Index)・クロス集計・横断検索・ドロップダウン検索・GridRows・ReloadRow・ゴミ箱一覧
HeavyConcurrency(同時 1)Userインポート・エクスポート・一括更新/移動/削除・物理削除・レコードの分割・サイトパッケージ・検索インデックス再構築・集計/計算式の同期
AdminFixedWindow(60 秒で 30 回)Userテナント・組織・グループ・ユーザー・管理者・システムログの管理画面
ApiTokenBucket(容量 30、毎秒 10 補充)Auto/api 配下の各 API コントローラーのクラス全体
ApiHeavyConcurrency(同時 1)ApiKeyAPI の一括削除・一括 Upsert・インポート・エクスポート・サイトパッケージ複製・集計の同期・検索インデックス再構築
AnonymousIpFixedWindow(60 秒で 60 回)Ipログイン画面・認証・ログイン時のパスワード変更・SSO の Challenge・パスキーの認証・デモ登録
PublicFormTokenBucket(容量 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
ApiKeyAuthorization: Bearer の値、なければフォームの parameters か JSON 本文の ApiKeyIP アドレス
AutoKeyResolver.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・ApiHeavyAPI と同じ形の 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 の設定はログ出力の拡張を参照してください。

イベント出る条件
RateLimitRejectedOn で拒否し、LogRejected: true のとき
RateLimitWouldHaveRejectedLogOnly で、On なら拒否していたとき
RateLimitMetricsSnapshotOn か LogOnly のポリシーがあるとき、60 秒ごとの集計
  • ログにはポリシー名・アルゴリズム・パーティション・パス・メソッド・ログイン ID などが入ります。API キーはそのまま出さず、SHA-256 の先頭 4 バイト(apikey:sha256:xxxxxxxx)にします。
  • 同じポリシー・同じキーの詳細ログは 10 秒に 1 件までに間引き、全体でも 10 秒あたり 50 件を超える分は件数だけの要約になります(BodyRateLimitLogThrottle)。

導入の手順(例) ​

  1. 全体の Mode を LogOnly にして再起動し、しばらく運用して ratelimitlogs.json の RateLimitWouldHaveRejected を見る。
  2. 正当な操作が引っかかるポリシーは上限を上げる。連携用のアカウントは Exclusions.LoginIds に入れる。
  3. Mode を On にして再起動する。

関連ページ ​

変更履歴

第1版レートリミッターの解説と、フォーム投稿の制限・ウイルススキャン・拡張子制限・外部検索エンジン・管理画面設定の改修・設計メモを追加