Locust(ローカスト)は、負荷試験のシナリオをPythonのコードで書くオープンソースのツールです。この記事では2026年9月時点の最新版 Locust 2.46.5 を手元で動かし、ログイン付きAPIへの負荷試験を例に、シナリオの書き方からヘッドレス実行、段階的な負荷のかけ方、CIでの合否判定、分散実行までを実行結果つきで解説します。
まとめ:Locustで負荷試験を始める要点
- Locustは負荷試験用のPythonライブラリ兼コマンドです。最新版は2.46.5(2026年9月7日公開)で、Python 3.11以上が必要です。ライセンスはMITです。
- 導入は
pip install locust、実行はlocust -f locustfile.pyです。Web UIは既定で8089番ポートに立ち上がります。 - CIでは
--headless -u -r -tで条件を固定し、--csvと--htmlで結果を残します。失敗リクエストが1件でもあると終了コードは1になります。 - 秒間リクエスト数(RPS)は直接指定せず、ユーザー数と1サイクルの時間(待ち時間+応答時間)から決まります。ユーザーごとのタスク実行回数を秒間の目標値に合わせたいときは
constant_throughputを使います。 - 1プロセスで使えるCPUは1コアまでです。負荷が足りないときは、まず
FastHttpUserに切り替え、それでも足りなければmaster/worker構成でコア数だけworkerを増やします。
Locustの特徴とJMeter・k6との違い
Pythonのコードでシナリオを書く設計
Locustでは、仮想ユーザーの振る舞いを User クラスとして書き、その中のメソッドを @task でタスクに登録します。GUIでテスト計画を組み立てるJMeterと違い、ログイン、トークンの付け替え、テストデータの読み込みといった処理を普通のPythonで書けます。シナリオがただのコードなので、Gitでの差分レビューもしやすくなります。
公式リポジトリは locustio/locust で、ライセンスはMITです。Python製のため、1プロセスは1コアしか使えません(GILによる制約)。高い負荷をかけるにはプロセスを増やす前提で設計されています。
JMeter・k6・Gatlingとの選び分け
| ツール | シナリオの書き方 | 本体の言語 | ライセンス |
|---|---|---|---|
| Locust | Pythonコード | Python | MIT |
| Apache JMeter | GUIで作るテスト計画 | Java | Apache-2.0 |
| Grafana k6 | JavaScript・TypeScript | Go | AGPL-3.0 |
| Gatling | Java・Kotlin・Scala・JavaScript・TypeScript | Scala | Apache-2.0 |
チームの主な言語がPythonで、認証や前処理を含む込み入ったシナリオを書くならLocustが合います。逆に、テスト担当者がコードを書かない体制なら、Locustを第一候補にすべきではありません。また、Locustの1プロセスは1コアまでしか使えないため、高いRPSが必要な試験ではworkerを複数並べる構成が前提になります。JavaScriptでシナリオを書き、閾値で合否を判定する使い方は k6の記事 で、HTTP/2の性能試験は JMeterのHTTP/2プラグインの解説 で扱っています。
Locustのインストールと動作確認
pipで入れるのが基本です。WindowsでもmacOS・Linuxと同じ手順で入ります。システムのPythonを汚さないよう、仮想環境に入れるのが無難です。
python3 -m venv .venv
source .venv/bin/activate # Windowsは .venv\Scripts\activate
pip install locust==2.46.5 # 最新版を入れるなら版指定を外す
locust --version
# locust 2.46.5 from .../site-packages/locust (Python 3.13.14)
Python 3.10以前ではLocust 2.46.5を入れられません。PyPIでの要件が >=3.11 になっているためです。Dockerを使う場合は公式イメージ locustio/locust に、カレントディレクトリのlocustfileをマウントして起動します。
docker run -p 8089:8089 -v $PWD:/mnt/locust locustio/locust -f /mnt/locust/locustfile.py
locustfileの書き方:ログイン付きAPIのシナリオ
HttpUserとタスクの重み
次のコードは、ログインしてトークンを受け取り、商品APIと遅いAPIを3対1の比率で呼ぶシナリオです。on_start は仮想ユーザーごとに1回だけ実行されるので、ログインはここに書きます。
import random
from locust import HttpUser, task, between
class ApiUser(HttpUser):
wait_time = between(1, 2)
def on_start(self):
res = self.client.post("/api/login", json={"user": "demo", "password": "secret"})
self.client.headers["Authorization"] = f"Bearer {res.json()['token']}"
@task(3)
def get_item(self):
item_id = random.randint(1, 100)
with self.client.get(f"/api/items/{item_id}", name="/api/items/[id]", catch_response=True) as res:
if res.status_code == 404:
res.success()
@task(1)
def slow(self):
self.client.get("/api/slow")
@task(3) と @task(1) の数字は選ばれる確率の重みです。wait_time = between(1, 2) は、タスクを1回終えるごとに1〜2秒待つ指定です。self.client は requests のセッションと同じように扱え、Cookieやヘッダーはユーザーごとに保持されます。
URLの集約と、404を成功として数える判定
IDをURLに埋め込むと、統計がURLごとに別々の行へ分かれます。name="/api/items/[id]" を渡すと1行にまとまります。
存在しないIDへの404も、既定では失敗として数えられます。仕様どおりの応答なら、catch_response=True で応答を受け取り、res.success() を呼んで成功扱いにします。反対に、ステータスが200でも本文にエラーが入っている場合は res.failure("理由") で失敗にできます。
ヘッドレス実行とWeb UIの使い分け
コマンドラインの主要オプション
locust -f locustfile.py だけで起動すると、Web UI(http://localhost:8089)でユーザー数と生成レートを入力してから開始します。CIや再現性が必要な計測では、--headless で画面を使わずに条件を固定します。
locust -f locustfile.py -H http://127.0.0.1:8099 \
--headless -u 20 -r 5 -t 20s \
--csv out/run1 --html report.html --only-summary
| オプション | 意味 |
|---|---|
-H |
負荷をかける先のベースURL |
-u |
最大同時ユーザー数 |
-r |
1秒あたりに増やすユーザー数 |
-t |
実行時間(30s、20m、1h30mなど) |
--headless |
Web UIを使わずに即時開始 |
--autostart |
Web UIを残したまま即時開始 |
--only-summary |
途中経過を出さず最後の集計だけ出力 |
コマンドラインの -t は、--headless か --autostart と組み合わせたときだけ有効です。Web UIから開始する場合は、開始フォームのRun time欄で実行時間を指定します。指定しなければ、止めるまで走り続けます。
Web UIのタブと見る場所
2.46.5のWeb UIには、Statistics、Charts、Failures、Exceptions、Current Ratio、Download Data、Logsのタブがあります。分散実行のときはWorkersタブも加わります。実行中に見るのは主に次の3つです。
- Statistics:リクエスト名ごとの件数、失敗数、中央値、95パーセンタイル、RPS
- Charts:RPS、応答時間、ユーザー数の推移。ユーザー数を増やしてもRPSが伸びず応答時間だけ上がり始めた点が、限界の目安です。ただし負荷生成側のCPUが足りない場合も同じ形になるので、Locustのコンソールに出るCPU使用率の警告も確認します
- Failures:失敗の種類と件数。ステータスコードや例外メッセージ単位で集計されます
Download DataからはCSVとHTMLレポートを取得できます。
RPSの決まり方とconstant_throughputによる調整
Locustで指定するのはユーザー数で、RPSは直接指定しません。1タスクで1リクエストを送るシナリオなら、RPSは「ユーザー数 ÷ 1サイクルの時間(待ち時間+応答時間)」がおおよその目安です。1タスクで複数のリクエストを送る場合は、その本数を掛けて考えます。先ほどのシナリオを20ユーザー(1秒に5人ずつ増加)で20秒実行すると、次の集計になりました。
Type Name # reqs # fails | Avg Min Max Med | req/s
GET /api/items/[id] 144 0(0.00%) | 6 1 39 5 | 7.40
POST /api/login 20 0(0.00%) | 9 4 14 9 | 1.03
GET /api/slow 69 0(0.00%) | 432 304 478 450 | 3.55
Aggregated 233 0(0.00%) | 132 1 478 6 | 11.98
待ち時間の平均は1.5秒なので、20ユーザーがそろった状態の目安は約13 req/sです。実測の合計11.98 req/sには、20人に達するまでの約3秒間と、ログインの20件が含まれています。そのうえで、/api/slow の応答に約0.45秒かかるぶんサイクルが長くなり、その分だけRPSが下がります。つまり、対象システムが遅くなるとRPSも下がります。「ユーザー数を固定して応答時間を見る試験」ではこれが自然ですが、「毎秒20件を処理できるか」を確かめたい試験では条件がぶれます。
RPSを目標値に近づけたいときは、wait_time = constant_throughput(2) のように、1ユーザーが1秒間にタスクを何回実行するかを指定します。調整されるのはリクエスト数ではなくタスクの実行回数です。商品APIを呼ぶタスク1つだけのシナリオに変えて、10ユーザー・15秒で実行すると、商品APIは20.38 req/sになりました(ログインを含む合計は21.06 req/s)。ただし、これは待ち時間を詰めて目標に近づける仕組みです。タスクの実行に0.5秒以上かかると待ち時間は0になり、目標のRPSには届きません。
LoadTestShapeによるramp-upと段階的な負荷
-r は一定の割合でユーザーを増やすだけです。「5人で10秒、15人に増やして10秒、その後0人」のような段階的な負荷は、LoadTestShape を継承したクラスで書きます。Locustは tick() を約1秒ごとに呼び、戻り値の「ユーザー数と生成レート」に合わせて負荷を調整します。None を返すとテストが終わります。
from locust import HttpUser, LoadTestShape, task, constant
class WebUser(HttpUser):
wait_time = constant(1)
@task
def index(self):
self.client.get("/")
class StepShape(LoadTestShape):
# (終了秒, ユーザー数, 生成レート)
stages = [(10, 5, 5), (20, 15, 5), (30, 0, 5)]
def tick(self):
run_time = self.get_run_time()
for end, users, rate in self.stages:
if run_time < end:
return (users, rate)
return None
locustfileにShapeクラスがあると、-u -r -t は不要で、接続先の -H と --headless だけで実行できます。実行ログでは、開始直後に5人、10秒後に15人、20秒後に0人へ切り替わり、30秒の時点で終了しました。
Shape test updating to 5 users at 5.00 spawn rate
Shape test updating to 15 users at 5.00 spawn rate
Shape test updating to 0 users at 5.00 spawn rate
結果レポートの読み方:CSV・HTML・JSON
--csv out/run1 を付けると、2.46.5では次の4ファイルが出力されました。--help の説明文には3ファイルと書かれていますが、実際には例外用のファイルも作られます。
| ファイル | 内容 |
|---|---|
run1_stats.csv |
リクエスト名ごとの最終集計 |
run1_stats_history.csv |
時系列の集計(グラフ用) |
run1_failures.csv |
失敗の種類と発生回数 |
run1_exceptions.csv |
locustfile内で発生した例外 |
run1_stats.csv の列は Request Count、Failure Count、Median Response Time、Requests/s に続き、50%から100%までのパーセンタイルが並びます。応答時間の単位はミリ秒です。性能要件がパーセンタイルで決まっているなら、95%や99%の列と照らし合わせます。平均は少数の遅いリクエストに引っ張られるためです。今回の集計(Aggregated)では、平均132msに対して中央値は6msでした。
1ファイルで共有したいなら --html report.html でグラフ付きのHTMLレポートを出力します。ファイル名の {u} {r} {t} は、ユーザー数、生成レート、実行時間に置き換わります。別のプログラムで集計する場合は、--json(標準出力)か --json-file で最終集計をJSONとして受け取れます。
CIでの合否判定と終了コードの制御
ヘッドレス実行では、失敗リクエストが1件でもあるとLocustは終了コード1で終わります。商品APIの404を失敗のまま数えるシナリオで試すと、失敗率10.69%で Shutting down (exit code 1) になりました。GitHub ActionsなどのCIでは、このステップがそのまま失敗します。
「失敗は1%まで許す」「95パーセンタイルが800msを超えたら落とす」といった条件にしたい場合は、quitting イベントで environment.process_exit_code を設定します。
import logging
from locust import events
@events.quitting.add_listener
def check_result(environment, **kwargs):
stats = environment.stats.total
if stats.num_requests == 0:
logging.error("リクエストが1件も記録されていません")
environment.process_exit_code = 1
elif environment.runner.exceptions:
logging.error("locustfile内で例外が発生しました")
environment.process_exit_code = 1
elif stats.fail_ratio > 0.01:
logging.error("失敗率が1%を超えました")
environment.process_exit_code = 1
elif stats.get_response_time_percentile(0.95) > 800:
logging.error("95パーセンタイルが800msを超えました")
environment.process_exit_code = 1
else:
environment.process_exit_code = 0
process_exit_code に値を入れると、Locust標準の判定(失敗が1件でもあれば1)より優先されます。1%以下の失敗を許すには合格時に0を入れる必要がありますが、そのままではlocustfile内の例外も見逃します。そのため、この例では environment.runner.exceptions を先に確認し、リクエストが1件も記録されない場合(ユーザー数0など)も不合格にしています。
このコードで試すと、失敗率0.53%(1,500件中8件)では終了コード0、タスク内で例外を送出すると1、ユーザー数0でも1になりました。応答に約0.45秒かかるAPIだけを流して閾値を300msに下げた場合も1です。失敗があっても終了コードを変えたくないときは、--exit-code-on-error 0 を指定します(既定値は1)。性能要件を数値で決める手順は 非機能要件の数値化と閾値テストの解説 を参照してください。
分散実行:masterとworkerの構成
masterとworkerの起動手順
分散実行では、--master を付けたプロセスがWeb UIと全体の制御を担当し、--worker を付けたプロセスが仮想ユーザーを動かして統計をmasterへ送ります。master自身はユーザーを動かしません。1プロセスは1コアしか使えないため、負荷生成マシンのコア数と同じ数だけworkerを立てます。
# 負荷生成マシンA(master)
locust -f locustfile.py --master -H https://api.example.com \
--headless -u 1000 -r 50 -t 10m --expect-workers 4
# 負荷生成マシンB・C(workerを2つずつ)
locust -f locustfile.py --worker --master-host 192.168.1.10
--expect-workers 4 は、workerが4つ接続するまで開始を待つ指定です。-u や -r はmaster側だけに付ければ、workerへ配分されます。masterとworkerは5557番ポートで通信するので、ファイアウォールで開けておきます。
1台のマシンでコア数分を使うだけなら、--processes で自動的にプロセスを分けられます。--processes 2 で実行したところ、workerが2つ接続されて負荷を分担しました。ただしこのオプションはWindowsでは使えません。この構成でも終了コードはmasterのものが返り、失敗があるときは1になりました。
Docker ComposeとKubernetes Operator
Docker Composeでは、公式イメージをmasterとworkerの2サービスとして定義します。公式ドキュメントの例では -H がmaster自身のWeb UI(http://master:8089)になっているので、試験対象のURLに書き換えて使います。
services:
master:
image: locustio/locust
ports:
- "8089:8089"
volumes:
- ./:/mnt/locust
command: -f /mnt/locust/locustfile.py --master -H https://api.example.com
worker:
image: locustio/locust
volumes:
- ./:/mnt/locust
command: -f /mnt/locust/locustfile.py --worker --master-host master
workerを増やすときは docker compose up --scale worker=4 とします。Kubernetesでは、公式ドキュメントに載っているLocust Operatorが使えます。master/workerのJob作成、locustfileのマウント、Web UIの公開、メトリクス収集までをカスタムリソースで管理できます。なお、マネージドサービスのLocust Cloudは終了に向かっており、2.43.0(2025年12月30日)で本体からの参照が削除されました。
FastHttpUserで負荷生成側の性能を上げる
workerを増やす前に試したいのが FastHttpUser です。HttpUser と同じAPIのまま、内部のHTTPクライアントを python-requests から geventhttpclient に切り替えます。公式ドキュメントでは、小さなリクエストを待ち時間なしで繰り返す最良のケースで、1プロセスあたり HttpUser が約4,000 req/s、FastHttpUser が約16,000 req/sとされています(2021年のM1 MacBook Pro、Python 3.11で計測)。
1ユーザー・待ち時間0で10秒ずつ比べた結果は、HttpUser が463.88 req/s、FastHttpUser が854.88 req/sで、約1.8倍でした(Intel Core i9-9880H、検証用サーバーはPython標準の ThreadingHTTPServer を同じマシンで稼働)。公式の4倍とは、マシン、Pythonの版、応答側サーバーの条件がいずれも異なるため、倍率は環境ごとに測り直す必要があります。負荷生成側のCPUが先に張り付くかどうかは、試験中に top などで確認します。
切り替えは from locust import FastHttpUser にして継承元を変えるだけです。ただし requests 固有の機能(セッションのアダプター設定など)に依存したコードは、そのままでは動きません。
HTTP以外のプロトコルとlocust-plugins
Locustの計測はHTTPに限りません。任意の処理の所要時間を events.request.fire で報告すれば、Statisticsに1行として並びます。次は、TCP接続にかかる時間を計測する例です。
import socket
import time
from locust import User, task, constant
class TcpUser(User):
wait_time = constant(1)
@task
def connect(self):
start = time.perf_counter()
exc = None
try:
with socket.create_connection(("127.0.0.1", 8099), timeout=2):
pass
except OSError as e:
exc = e
self.environment.events.request.fire(
request_type="TCP",
name="connect",
response_time=(time.perf_counter() - start) * 1000,
response_length=0,
exception=exc,
context={},
)
実行すると、集計に TCP connect の行が追加され、HTTPのリクエストと同じ形式で件数と応答時間が出ました。HTTP以外のプロトコルについて、公式に用意されているものは次のとおりです。
- gRPC:本体に同梱されたクラスではなく、公式ドキュメントとリポジトリの
examples/grpcに、利用者が組み込むGrpcUser基底クラスの例があります - Socket.IO:本体同梱の
SocketIOUser(実験的機能) - MQTT:本体同梱の
MqttUser(実験的機能)
Socket.IOではない素のWebSocketには、公式ドキュメントに専用クラスがありません。WebSocketクライアントのライブラリを上の例と同じ形で包み、送信から応答までの時間を events.request.fire で報告します。
コミュニティ製の拡張集 locust-plugins(SvenskaSpel、2026年6月に5.0.3)には、Playwright・Selenium・Kafka・FTP用のユーザー、CSVやMongoDBからテストデータを読むリーダー、--check-rps や --check-fail-ratio による合否判定オプションなどがあります。
よくある質問
Locustの読み方は?
「ローカスト」と読みます。英単語のlocustは「イナゴ」の意味で、大量の仮想ユーザーが一斉にシステムへ押し寄せる様子に由来します。
LocustはWindowsで使えますか?
使えます。pip install locust で導入でき、Web UI、ヘッドレス実行、master/workerの分散実行も動きます。1台で自動的にプロセスを分ける --processes だけはWindowsで使えないため、複数コアを使うときはworkerを個別に起動します。
Locustの負荷試験でramp-upを設定するには?
一定の割合で増やすだけなら、-r(1秒あたりに増やすユーザー数)を指定します。段階的に増減させるなら、LoadTestShape を継承したクラスの tick() で、経過時間に応じたユーザー数と生成レートを返します。
Locustは有料ですか?
本体は無料です。MITライセンスのオープンソースで、商用の負荷試験にも使えます。有料のマネージドサービスだったLocust Cloudは、2.43.0で本体からの参照が削除されています。
Locustとk6はどちらを選ぶべきですか?
シナリオをPythonで書きたいならLocust、JavaScriptやTypeScriptで書き、閾値による合否判定を標準機能で済ませたいならk6です。Locustで高いRPSが必要な場合は、workerを複数並べる前提になります。