- 技術 💻
Locustとは・使い方|Pythonで書く負荷テストを実行結果と失敗の原文で解説
結論
Locust(ローカスト)は、「仮想ユーザーが何をするか」を Python のクラスで書き、何人分でも同時に動かして Web サーバーや API の負荷テストをするオープンソースのツールです。pip install locust で入り、locust --headless(画面なし)と locust(ブラウザの画面)の2通りで動かせます。
この記事でわかること
- locustfile の書き方(HttpUser・@task・wait_time・on_start)と、headless・Web UI で実行した実物
- RPS・p50/p95/p99・失敗率の読み方と、1〜100ユーザーで限界を探した実測の表
- Connection refused・Read timed out・404/500 の原文、分散実行・CSV・CI での合否判定、k6・JMeter との判断表
Locust は「locustfile(Python)に書いた行動」を、指定した人数ぶん同時に繰り返し、結果を集計して表・グラフ・CSV にします。全体の流れは次のとおりです。
flowchart TD
F[locustfile.py<br>HttpUser と @task] --> L[locust コマンド<br>-u 人数 -r 速さ -t 時間]
L --> U[仮想ユーザー 1〜N<br>タスクを繰り返す]
U --> S[(テスト対象の<br>サーバー)]
S --> U
U --> R[集計<br>表・Web UI・CSV]
よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| Locust とは | Python でシナリオを書く負荷テストツール(MIT ライセンス) | Locustとは |
| locust 使い方(最短) | pip install locust → locustfile.py を書く → locust --headless -u 10 -r 2 -t 30s |
インストール/headless |
| locustfile の書き方 | HttpUser を継承し、@task のメソッドで self.client.get() を呼ぶ | locustfileの書き方 |
| Web UI の開き方 | locust で起動し、ブラウザで http://localhost:8089 |
Web UIで実行 |
| 結果の見方 | 平均ではなく p95・p99 と失敗率を見る | 結果の読み方 |
| RPS が上がらない | 対象サーバーの限界か、Locust 側の CPU(1プロセス≒1コア) | 限界を探す/分散実行 |
| Connection refused が出る | 対象サーバーが起動していない・ホスト名やポートが違う | 失敗の原文 |
| k6・JMeter との違い | Python で書くなら Locust、JS なら k6、GUI なら JMeter | 判断表 |
確認環境:Ubuntu 24.04.5 LTS(2コア・メモリ 7GB)の上で、Python 3.13.18 の venv に Locust 2.47.0(gevent 26.9.0・geventhttpclient 2.6.1・requests 2.34.2)を pip で入れました。テスト対象は FastAPI 0.162.0 を uvicorn 0.54.0(1プロセス)で 127.0.0.1:8000 に起動した自作 API で、負荷はこの自作サーバーにだけ向けています。headless 実行・分散実行(--processes と master/worker)・CSV・HTML レポート・locust.conf はこの環境で実行し、Web UI は Chromium 153.0.8010.12(playwright 1.63.0)で開いて画面の文言と数値を読み取りました。Locust の仕様(オプション・FastHttpUser・終了コード)は公式ドキュメント(docs.locust.io の stable 版)と PyPI で確かめています。Locust と対象サーバーが同じ 2コアの機械で動いているため、RPS の絶対値はこの環境の値で、対象サーバーの性能そのものではありません(確認日 2026年10月29日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Locustとは・使い方|Pythonで書く負荷テストを実行結果と失敗の原文で解説」
Locustとは(Pythonで書く負荷テストツール)
この節では、Locust が何をするツールで、ほかの負荷テストツールと何が違うのかが分かります。
Locust は、HTTP などのサーバーに「たくさんの利用者が同時に使っている状態」を作り、応答時間と失敗率を測るための負荷テストツールです。 PyPI の説明では「HTTP やほかのプロトコルのための、オープンソースの性能/負荷テストツール」で、「テストを普通の Python コードで定義できる」のが特徴とされています。名前の locust は「群れで飛ぶバッタ」から来ていて、仮想ユーザーの群れがサーバーに押し寄せる様子をたとえたものです(公式ドキュメント「What is Locust?」)。
1シナリオを Python で書く
XML や GUI の設定ではなく、クラスとメソッドで「ログインして一覧を見て詳細を開く」を書く。if や for、ランダムな値、外部ライブラリがそのまま使える。
21プロセスで数千ユーザー
gevent(軽量スレッド)で動くため、1プロセスでも数千の仮想ユーザーを持てる。足りなければ複数プロセス・複数台に分散できる。
3画面ありと画面なしの両方
ブラウザの Web UI でグラフを見ながら人数を変えられる。CI では --headless で画面なしに動かし、終了コードで合否を返せる。
| 項目 | 内容(2026年10月29日時点) |
|---|---|
| 最新版 | 2.47.0(2026年10月21日公開) |
| 対応する Python | 3.11 以上(PyPI の Requires: Python) |
| ライセンス | MIT License |
| HTTP クライアント | HttpUser(requests)と FastHttpUser(geventhttpclient) |
| 対象 | 主に Web サイト・HTTP API。クライアントを自作すればほかのプロトコルも |
負荷テストそのものの進め方(スモーク→負荷→ストレス→耐久)と k6 での書き方は、関連記事「負荷テストの進め方とk6の使い方|スクリプト例と結果の読み方」で扱っています。この記事は「Locust というツールの使い方と、出力の読み方」に絞ります。
Locustのインストール(pip・venv)と動作確認
この節では、Locust を入れて locust -V が通るまでの手順と、入る依存パッケージが分かります。
仮想環境を作って pip install locust を実行するだけです。 手元では venv の大きさが 71MB(FastAPI・uvicorn を含む)になりました。
仮想環境を作る
python3 -m venv venv → source venv/bin/activate(Windows は venv\Scripts\activate)。Python は 3.11 以上。
Locust を入れる
pip install locust。gevent・geventhttpclient・requests・Flask(Web UI 用)・pyzmq(分散実行用)などが一緒に入る。
版を確かめる
locust -V。版の数字と Python の版が出れば完了。
テスト対象を用意する
自分が管理するサーバーを 127.0.0.1 に起動する。この記事では FastAPI の自作 API を uvicorn で 8000 番に起動した。
$ python3 -m venv venv
$ source venv/bin/activate
$ pip install locust
$ locust -V
locust 2.47.0 from /dev/shm/t115/venv/lib/python3.13/site-packages/locust (Python 3.13.18)
$ pip list | grep -iE "^(locust|gevent|geventhttpclient|requests)"
gevent 26.9.0
geventhttpclient 2.6.1
locust 2.47.0
requests 2.34.2
venv の作り方と有効化の違い(OS 別)は、関連記事「Python venvの使い方|仮想環境の作成・有効化・削除をOS別にコピペ」で扱っています。
テスト対象にした自作 API
負荷をかける先として、FastAPI で次の API を作り、uvicorn app:app --host 127.0.0.1 --port 8000 で起動しました。/slow はわざと 100〜300ms 待ち、/flaky は 10% の確率で 500 を返します。
# app.py(抜粋)
import asyncio, random
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
ITEMS = {i: {"id": i, "name": f"item-{i}", "price": i * 100} for i in range(1, 101)}
class Login(BaseModel):
username: str
password: str
@app.post("/login")
async def login(body: Login):
if body.username == "demo" and body.password == "demo":
return {"token": "demo-token"}
raise HTTPException(status_code=401, detail="invalid credentials")
@app.get("/items")
async def list_items():
return list(ITEMS.values())
@app.get("/items/{item_id}")
async def get_item(item_id: int):
if item_id not in ITEMS:
raise HTTPException(status_code=404, detail="item not found")
return ITEMS[item_id]
@app.get("/slow")
async def slow():
await asyncio.sleep(random.uniform(0.1, 0.3))
return {"message": "slow done"}
@app.get("/flaky")
async def flaky():
if random.random() < 0.1:
raise HTTPException(status_code=500, detail="unlucky")
return {"message": "ok"}
FastAPI の作り方そのものは、関連記事「FastAPI入門|uvで作るCRUD API完成コードと実行結果」で扱っています。
locustfileの書き方(HttpUser・@task・wait_time・on_start)
この節では、Locust のシナリオファイル(locustfile)の部品と、それぞれが何を決めるのかが分かります。
locustfile は「1人の仮想ユーザーの行動」をクラスで書いたものです。 HttpUser を継承したクラスを作り、@task を付けたメソッドが「その人がする操作」、wait_time が「操作と操作の間の待ち時間」、on_start が「その人が最初に1回だけすること」です。この記事で使った locustfile は次のとおりです。
# locustfile.py
import random
from locust import HttpUser, task, between
class ShopUser(HttpUser):
# 各タスクの間に 1〜3 秒待つ(人の操作の間隔のつもり)
wait_time = between(1, 3)
host = "http://127.0.0.1:8000"
def on_start(self):
# ユーザーが生まれたときに1回だけログインしてトークンを持つ
res = self.client.post("/login", json={"username": "demo", "password": "demo"})
self.token = res.json()["token"]
self.client.headers["Authorization"] = f"Bearer {self.token}"
@task(5)
def list_items(self):
self.client.get("/items")
@task(3)
def view_item(self):
item_id = random.randint(1, 100)
# URL が毎回変わるので、name で1つの行にまとめる
self.client.get(f"/items/{item_id}", name="/items/[id]")
@task(1)
def create_item(self):
self.client.post("/items", json={"name": "new", "price": 500})
@task(1)
def slow_page(self):
# 応答が 500ms を超えたら失敗として数える
with self.client.get("/slow", catch_response=True) as res:
if res.elapsed.total_seconds() > 0.5:
res.failure(f"too slow: {res.elapsed.total_seconds():.3f}s")
| 部品 | 役割 | 上の例での値 |
|---|---|---|
HttpUser |
HTTP を話す仮想ユーザーの土台。self.client(requests.Session の包み)を持ち、Cookie を保つ |
ShopUser |
host |
リクエストの前に付く URL。--host や Web UI の Host 欄で上書きできる |
http://127.0.0.1:8000 |
wait_time |
タスクとタスクの間の待ち時間 | between(1, 3) |
@task(重み) |
その人がする操作。重みが大きいほど選ばれやすい | 5:3:1:1 |
on_start/on_stop |
ユーザーの開始時・終了時に1回だけ実行 | ログイン |
name= |
URL が変わるリクエストを1つの統計行にまとめる | /items/[id] |
catch_response=True |
自分で成功・失敗を決める(res.failure()/res.success()) |
500ms 超を失敗に |
重みは「タスクが選ばれる割合」です。Web UI の「Current ratio」タブでは、上の locustfile が listItems 50.0%・viewItem 30.0%・createItem 10.0%・slowPage 10.0% と表示されました(5:3:1:1 を百分率にしたものと一致)。
wait_time の4種類
待ち時間は「人の操作の間隔を真似る」ためのものです。公式ドキュメントの4種類を、使いどころと一緒にまとめます。
between(a, b)
a〜b 秒のランダム。人がページを読む間隔に近い。
いちばんよく使う
constant(s)
毎回 s 秒。constant(0) で「応答が返ったら即次」。限界を探すときに。
この記事の RPS 測定
constant_throughput(n)
1ユーザーが毎秒 n 回まで実行するよう待ちを調整。人数×n が目標 RPS。
RPS を決めたいとき
constant_pacing(s)
タスク開始から次の開始までを s 秒に揃える。応答が遅くても間隔を保つ。
周期的なポーリング
「成功」の既定は「ステータスコードが 400 未満」です(公式ドキュメント「Requests are considered successful if the HTTP response code is OK (<400)」)。200 が返っても内容が違う・遅すぎる、を失敗にしたいときは catch_response=True で自分で決めます。
headlessで実行する(locust --headless の出力の読み方)
この節では、画面なし(headless)で Locust を動かすコマンドと、流れてくる表の読み方が分かります。
locust -f locustfile.py --headless -u 20 -r 5 -t 30s で、20ユーザーを毎秒5人ずつ増やし、30秒で止まります。 手元の実行の先頭と末尾は次のとおりです(表の幅は詰めています)。
$ locust -f locustfile.py --headless -u 20 -r 5 -t 30s --csv result/run1 --html result/run1.html
[2026-10-30 20:12:06,934] vm/INFO/locust.main: Starting Locust 2.47.0
[2026-10-30 20:12:06,935] vm/INFO/locust.main: Run time limit set to 30 seconds
[2026-10-30 20:12:06,950] vm/INFO/locust.runners: Ramping to 20 users at a rate of 5.00 per second
[2026-10-30 20:12:09,956] vm/INFO/locust.runners: All users spawned: {"ShopUser": 20} (20 total users)
Type Name # reqs # fails | Avg Min Max Med | req/s failures/s
--------|---------------|--------|---------|-----|----|-----|-----|------|----------
GET /items 17 0(0.00%) | 7 2 43 3 | 1.50 0.00
POST /items 6 0(0.00%) | 19 2 43 10 | 1.50 0.00
GET /items/[id] 8 0(0.00%) | 11 2 42 8 | 1.00 0.00
POST /login 20 0(0.00%) | 13 4 99 9 | 5.00 0.00
GET /slow 2 0(0.00%) | 150 115 185 185 | 0.00 0.00
--------|---------------|--------|---------|-----|----|-----|-----|------|----------
Aggregated 53 0(0.00%) | 17 2 185 8 | 9.00 0.00
(2秒ごとに同じ表が流れる)
[2026-10-30 20:12:36,935] vm/INFO/locust.main: --run-time limit reached, shutting down
[2026-10-30 20:12:37,067] vm/INFO/locust.main: Shutting down (exit code 0)
Type Name # reqs # fails | Avg Min Max Med | req/s failures/s
--------|---------------|--------|---------|-----|----|-----|-----|------|----------
GET /items 151 0(0.00%) | 4 2 43 3 | 5.10 0.00
POST /items 27 0(0.00%) | 6 1 43 2 | 0.91 0.00
GET /items/[id] 80 0(0.00%) | 3 1 42 2 | 2.70 0.00
POST /login 20 0(0.00%) | 13 4 99 9 | 0.68 0.00
GET /slow 31 0(0.00%) | 199 104 302 200 | 1.05 0.00
--------|---------------|--------|---------|-----|----|-----|-----|------|----------
Aggregated 309 0(0.00%) | 24 1 302 3 | 10.43 0.00
Response time percentiles (approximated)
Type Name 50% 66% 75% 80% 90% 95% 98% 99% 99.9% 100% # reqs
--------|--------------|----|----|----|----|----|----|----|----|------|----|------
GET /items 3 3 4 4 6 8 15 40 44 44 151
POST /login 9 11 12 13 14 100 100 100 100 100 20
GET /slow 200 230 260 260 280 290 300 300 300 300 31
Aggregated 3 4 6 8 100 200 260 280 300 300 309
| 列 | 意味 | 読み方 |
|---|---|---|
| # reqs/# fails | リクエスト数と失敗数(割合) | 失敗が 0 でない行から見る |
| Avg/Min/Max/Med | 応答時間の平均・最小・最大・中央値(ms) | Avg と Med が離れていたら、遅い応答が混ざっている |
| req/s | 直近の秒間リクエスト数(RPS) | ユーザーを増やしても伸びなくなった点が限界 |
| percentiles の 95%・99% | 95%・99% の応答がこの値以下(ms) | 合否はここで決める(平均では決めない) |
| exit code | 終了コード。失敗が1件でもあると 1 | CI の合否に使える(--exit-code-on-error で変更可) |
POST /login が 20 回ちょうどなのは、on_start が「ユーザー1人につき1回」だからです。/slow の中央値 200ms はサーバーの sleep(0.1〜0.3) のとおりで、Locust 自身が遅延を足していないことも分かります。
| オプション | 意味 |
|---|---|
-f/--locustfile |
locustfile のパス。省略すると ./locustfile.py |
-H/--host |
対象の URL。クラスの host より優先 |
-u/--users |
最大ユーザー数 |
-r/--spawn-rate |
毎秒何人ずつ増やすか |
-t/--run-time |
実行時間(30s・5m・1h30m)。開始時点から数える |
--only-summary |
2秒ごとの表を出さず、最後だけ出す |
-s/--stop-timeout |
停止時に実行中のタスクを待つ秒数 |
--csv/--html |
CSV と HTML レポートの出力先 |
Web UI(ブラウザ)で実行する:画面の見方と操作
この節では、Locust の Web UI を起動して、画面で人数を決めて開始・停止するまでと、各タブに何があるかが分かります。
locust -f locustfile.py と打つと http://localhost:8089 に画面が立ち上がり、ブラウザから人数・増やす速さ・対象 URL を入れて START を押します。 手元では --web-host 127.0.0.1 を付け、自分の機械からだけ開けるようにしました(既定は全てのアドレスで待ち受けます)。
$ locust -f locustfile.py --web-host 127.0.0.1
[2026-10-30 20:18:35,181] vm/INFO/locust.main: Starting Locust 2.47.0
[2026-10-30 20:18:35,182] vm/INFO/locust.main: Starting web interface at http://127.0.0.1:8089, press enter to open your default browser.
Chromium で開いた最初の画面(Start new load test)には、次の欄がありました。
| 画面の欄 | 意味 | コマンドの対応 |
|---|---|---|
| Number of users (peak concurrency) | 最大ユーザー数(既定 1) | -u |
| Ramp up (users started/second) | 毎秒増やす人数(既定 1)。以前の版の Spawn rate・Hatch rate | -r |
| Host | 対象の URL。locustfile の host が入っていた | -H |
| Advanced options → Run time | 実行時間(20、20s、3m、1h20m など) | -t |
| Advanced options → Profile | 実行に付けるラベル(任意) | --profile |
20ユーザー・毎秒5人・30秒で START を押すと、上部のヘッダーが Status RUNNING/Users 20/RPS 10.75/Failures 0% に変わり、EDIT(人数の変更)・STOP・RESET のボタンが出ました。タブは STATISTICS・CHARTS・FAILURES・EXCEPTIONS・CURRENT RATIO・DOWNLOAD DATA・LOGS の7つです。
| タブ | 見えるもの(手元の実物) |
|---|---|
| STATISTICS | 行ごとに # Requests・# Fails・Median・95%ile・99%ile・Average・Min・Max・Average size・Current RPS・Current Failures/s。/items は 51 件・中央値 4ms・95%ile 14ms |
| CHARTS | 3つの折れ線:Total Requests per Second(RPS と Failures/s)、Response Times(50th と 95th percentile)、Number of Users。人数が 0→20 に上がる線と、RPS が 10 前後で平らになる線が見えた |
| FAILURES | # Failures・Method・Name・Message・First Seen・Last Seen。失敗 0 件なので表は空 |
| EXCEPTIONS | locustfile の中で起きた Python の例外(# occurrences・Message・Traceback) |
| CURRENT RATIO | クラスとタスクの割合。ShopUser 100%、listItems 50.0%・viewItem 30.0%・createItem 10.0%・slowPage 10.0% |
| DOWNLOAD DATA | Download requests CSV・failures CSV・exceptions CSV・Report(HTML)の4つのリンク |
| LOGS | Master Logs。「Ramping to 20 users at a rate of 5.00 per second」などコンソールと同じ行 |
STOP を押すと Status が STOPPED になり、ボタンが NEW・RESET に変わります。止めた時点の STATISTICS は、/items が 149 件・中央値 3ms・95%ile 11ms・99%ile 24ms、/slow が 26 件・中央値 180ms・95%ile 280ms で、headless で同じ条件を走らせた結果とほぼ同じでした。
Web UI が向く場面
初めてのシナリオで、動き方をグラフで見たい
走らせながら人数を変えて限界を探したい(EDIT)
失敗の文言をその場で見て locustfile を直したい
headless が向く場面
CI やスケジュールで毎回同じ条件で回す
結果を CSV・終了コードで機械的に扱う
サーバーにブラウザが無い・ポート 8089 を開けたくない
Web UI は /stats/requests という JSON の API も持っていて、手元で curl http://127.0.0.1:8089/stats/requests を叩くと state: running, user_count: 20, total_rps: 10.54 と各行の統計が返りました。自作の監視に取り込むときに使えます。
結果の読み方:RPS・p50/p95/p99・失敗率(負荷テスト 結果の見方)
この節では、Locust が出す数字のうち、何を見て「速い・遅い・合格・不合格」を決めるのかが分かります。
見る順番は「失敗率 → p95/p99 → RPS → 平均」です。 平均は少数の遅い応答に引っ張られ、逆に多数の速い応答に隠されるため、合否の基準には使いません。
- 1失敗率(# fails・Failures/s)1件でもあれば原因を見る。失敗した応答の時間は統計に混ざらないことがある
- 2p95・p99(95%ile・99%ile)「20人に1人」「100人に1人」が体験する遅さ。合否はここ
- 3RPS(req/s)とユーザー数の関係人数を増やしても RPS が伸びなくなった点が、その構成の限界
- 4中央値(p50)と平均(Avg)の差差が大きいほど分布が偏っている。Max だけ大きいのは最初の接続や GC のことが多い
- 5対象サーバー側の CPU・メモリ・ログLocust の数字だけでは「どこが」遅いかは分からない
上の headless 実行の Aggregated 行を見ると、中央値 3ms・平均 24ms・p95 200ms と大きくずれています。これは速い /items と遅い /slow が混ざっているためで、エンドポイント別の行で見ないと意味が取れない例です。Locust の percentile は「approximated(近似)」と書かれているとおり、応答時間を丸めて数えているので、1ms 単位の差を議論する用途には向きません。
p99 がベンチマークで持つ意味や、ウォームアップ・外れ値の扱いは、関連記事「ベンチマークの落とし穴8選|JMH・p99・ウォームアップ」で扱っています。
ユーザー数を増やして限界を探す(1〜100ユーザーの実測)
この節では、同じ API にユーザー数だけ変えて負荷をかけ、「RPS が伸びなくなる点」と「応答時間が伸び始める点」がどう見えるかが分かります。
待ち時間なし(wait_time = constant(0))で /items/7 を叩き続け、1・10・50・100 ユーザーで各20秒ずつ測りました。 50 ユーザーから 100 ユーザーに倍にしても RPS はほぼ同じで、代わりに応答時間が倍になっています。これが「限界に達した」形です。
# locust_rps.py
from locust import HttpUser, task, constant
class HammerUser(HttpUser):
wait_time = constant(0) # 応答が返ったらすぐ次を送る
host = "http://127.0.0.1:8000"
@task
def item(self):
self.client.get("/items/7", name="/items/[id]")
$ for u in 1 10 50 100; do
locust -f locust_rps.py --headless -u $u -r 50 -t 20s --only-summary --csv result/rps_$u
done
| ユーザー数 | RPS | p50(ms) | p95(ms) | p99(ms) |
|---|---|---|---|---|
| 1 | 575.8 | 1 | 4 | 6 |
| 10 | 628.3 | 9 | 28 | 65 |
| 50 | 1,025.6 | 31 | 66 | 110 |
| 100 | 1,084.1 | 59 | 110 | 160 |
公式ドキュメントのクイックスタートも同じ見方を説明しています。「ユーザーを増やしても RPS が上がらず応答時間だけ上がる」なら対象が飽和しており、「応答時間が平らなまま」なら、まだ人数を増やせる、という読み方です。ただし伸びない原因が Locust 側のこともあります。Python は1プロセスで1コアしか使えないため、Locust が CPU を使い切ると「CPU usage above 90%」の警告をログに出します(公式ドキュメント)。手元の実測ではこの警告は出ず、対象サーバー(uvicorn 1プロセス)が先に飽和していました。
「限界が分かったあと、サーバーを増やすか大きくするか」の考え方は、関連記事「スケールアップとスケールアウトの違い|負荷分散方式の一覧と選び方」で扱っています。
失敗の原文と原因(Connection refused・Read timed out・404・500)
この節では、Locust の Error report に出る文言を実際に起こして、それぞれ何が起きているのかが分かります。
失敗は Error report(headless の末尾)・FAILURES タブ・_failures.csv の3か所に同じ文言で出ます。 次の locustfile で、サーバーを止めた状態と、起動した状態の両方を走らせました。
# locust_fail.py
from locust import HttpUser, task, constant
class FailUser(HttpUser):
wait_time = constant(0.5)
host = "http://127.0.0.1:8000"
@task
def flaky(self):
self.client.get("/flaky") # 10% の確率で 500
@task
def not_found(self):
self.client.get("/items/999", name="/items/[missing]") # 404
@task
def timeout(self):
self.client.get("/verslow", timeout=1) # サーバーは 2 秒かかる
サーバーを止めたとき:ConnectionRefusedError
$ locust -f locust_fail.py --headless -u 2 -r 2 -t 5s --only-summary
Aggregated 20 20(100.00%) | 1 0 5 2 | 4.40 4.40
Error report
# occurrences Error
------------------|------------------------------------------------------------------
9 GET /verslow: ConnectionRefusedError(111, 'Connection refused')
4 GET /flaky: ConnectionRefusedError(111, 'Connection refused')
7 GET /items/[missing]: ConnectionRefusedError(111, 'Connection refused')
[2026-10-30 20:16:39,147] vm/INFO/locust.main: Shutting down (exit code 1)
サーバーを起動したとき:ReadTimeout・404・500
Error report
# occurrences Error
------------------|------------------------------------------------------------------
11 GET /items/[missing]: HTTPError('404 Client Error: Not Found for url: /items/[missing]')
9 GET /verslow: ReadTimeout(ReadTimeoutError("HTTPConnectionPool(host='127.0.0.1', port=8000): Read timed out. (read timeout=1)"))
1 GET /flaky: HTTPError('500 Server Error: Internal Server Error for url: /flaky')
| 原文 | 起きていること | 見直す場所 |
|---|---|---|
ConnectionRefusedError(111, 'Connection refused') |
そのホスト・ポートで誰も待ち受けていない | 対象サーバーの起動、host の綴り・ポート、127.0.0.1 と外向きアドレスの違い |
ReadTimeout(... Read timed out. (read timeout=1)) |
接続はできたが、timeout 秒以内に応答が返らない | timeout の値、対象の処理時間、負荷で詰まっていないか |
HTTPError('404 Client Error: Not Found ...') |
URL が存在しない | パスの綴り、name= で隠れている実際の URL |
HTTPError('500 Server Error: Internal Server Error ...') |
対象サーバーの内部エラー | 対象サーバーのログ(Locust 側には理由は出ない) |
GET /slow: too slow: 0.282s |
res.failure() で自分が付けた文言 |
基準値と、対象の設計 |
Error report の「GET /verslow」には name= で付けた名前が出るため、404 のときは「本当の URL が何だったか」が分かりません。手元でも、サーバーを前の版のまま起動していたせいで /verslow が 404 になり、最初は原因に気づきませんでした。name で丸めた URL の失敗は、locustfile 側で実際の URL をログに出すと追いやすくなります。
接続が拒否されるときの TCP の仕組みと切り分けは、既存の「Connection refused」の記事に詳しく、503・502 を返すサーバー側の事情は既存の「503 Service Unavailable」「502 Bad Gateway」の記事に譲ります。
分散実行(--processes と master/worker)とFastHttpUser
この節では、Locust 1プロセスで足りないときに、同じ機械で複数プロセスに分ける方法と、クライアントを軽いものに替える方法が分かります。
Locust は master(集計と画面)と worker(実際にリクエストを送る)に分かれ、同じ機械でも別の機械でも動きます。 同じ機械なら --processes N の1行で master と N 個の worker が立ちます。公式ドキュメントは「1コアにつき worker 1つ」を勧めています(--processes -1 でコア数を自動検出)。
--processes で同じ機械に2プロセス
$ locust -f locust_rps.py --headless -u 100 -r 50 -t 20s --processes 2 --only-summary
[2026-10-30 20:15:56,649] vm/INFO/locust.runners: vm_b40a07c1... (index 0) reported as ready. 1 workers connected.
[2026-10-30 20:15:56,652] vm/INFO/locust.runners: vm_198da546... (index 1) reported as ready. 2 workers connected.
[2026-10-30 20:15:57,562] vm/INFO/locust.runners: Sending spawn jobs of 100 users at 50.00 spawn rate to 2 ready workers
...
[2026-10-30 20:16:17,720] vm/INFO/locust.runners: The last worker quit, stopping test.
Aggregated 28970 0(0.00%) | 50 7 436 43 | 1440.11 0.00
master と worker を別々に起動(別の機械と同じ手順)
# 端末1:master(画面なし・worker 2つを待ってから開始)
$ locust -f locust_rps.py --master --master-bind-host 127.0.0.1 --headless -u 100 -r 50 -t 15s --expect-workers 2
# 端末2・3:worker(別の機械なら --master-host に master のアドレス)
$ locust -f locust_rps.py --worker --master-host 127.0.0.1
$ locust -f locust_rps.py --worker --master-host 127.0.0.1
# master のログ
[2026-10-30 20:20:14,310] vm/INFO/locust.runners: vm_6fc3e286... (index 0) reported as ready. 1 workers connected.
[2026-10-30 20:20:14,364] vm/INFO/locust.runners: vm_eee61cbb... (index 1) reported as ready. 2 workers connected.
[2026-10-30 20:20:15,334] vm/INFO/locust.runners: Sending spawn jobs of 100 users at 50.00 spawn rate to 2 ready workers
[2026-10-30 20:20:16,476] vm/INFO/locust.runners: All users spawned: {"HammerUser": 100} (100 total users)
Aggregated 21069 0(0.00%) | 50 4 347 43 | 1374.20 0.00
# worker のログ
[2026-10-30 20:20:30,372] vm/INFO/locust.runners: Got quit message from master, shutting down...
FastHttpUser(requests の代わりに geventhttpclient)
HttpUser を FastHttpUser に替えるだけで、Locust 側の CPU の使い方が軽くなります。公式ドキュメントは「1コアで HttpUser 約4,000 req/s に対し FastHttpUser 約16,000 req/s」の例を挙げつつ、「個々のリクエストが速くなるわけではない」と注意しています。
from locust import task, constant
from locust.contrib.fasthttp import FastHttpUser
class FastHammerUser(FastHttpUser):
wait_time = constant(0)
host = "http://127.0.0.1:8000"
@task
def item(self):
self.client.get("/items/7", name="/items/[id]")
| 構成(100ユーザー・20秒) | RPS | p50(ms) | p95(ms) | p99(ms) |
|---|---|---|---|---|
| HttpUser・1プロセス | 1,084.1 | 59 | 110 | 160 |
| HttpUser・--processes 2 | 1,440.1 | 43 | 100 | 150 |
| HttpUser・master+worker 2(15秒) | 1,374.2 | 43 | 120 | 200 |
| FastHttpUser・1プロセス | 1,854.8 | 47 | 87 | 170 |
この環境では Locust と対象サーバーが同じ2コアを取り合っているため、プロセスを増やしても対象サーバーの取り分が減り、伸びは 1.3 倍ほどに留まりました。本来は負荷をかける側と受ける側を別の機械に分け、Locust 側の CPU が足りているかを先に確かめます。 それでも、クライアントを軽くした FastHttpUser が最も RPS を出したのは、Locust 側の CPU がボトルネックの一部だったことを示しています。
--processesは fork() を使うため Windows では使えません(公式ドキュメント)。Windows では master と worker を別々に起動します。- worker 側の
-f -で locustfile を master から受け取れますが、1ファイルのときだけです。 - FastHttpUser は「ほぼ差し替え可能」ですが、Cookie を手で扱うなど requests の内部に触れているコードでは違いが出ることがあります。
CSV・HTMLレポート・locust.conf・CIでの合否判定
この節では、結果をファイルに残す方法と、毎回同じ条件で走らせる設定ファイル、CI で合否を終了コードにする方法が分かります。
--csv result/run1 で4つの CSV、--html result/run1.html で1ファイルの HTML レポートが出ます。 手元の出力は次のとおりでした。
$ ls -la result/
-rw-r--r-- 1 root root 954400 Oct 30 20:12 run1.html # グラフ入りの単体 HTML
-rw-r--r-- 1 root root 31 Oct 30 20:12 run1_exceptions.csv # locustfile の例外
-rw-r--r-- 1 root root 52 Oct 30 20:12 run1_failures.csv # 失敗の文言と回数
-rw-r--r-- 1 root root 1065 Oct 30 20:12 run1_stats.csv # 行ごとの集計(最終)
-rw-r--r-- 1 root root 5122 Oct 30 20:12 run1_stats_history.csv # 1秒ごとの推移
$ head -3 result/run1_stats.csv
Type,Name,Request Count,Failure Count,Median Response Time,Average Response Time,Min Response Time,Max Response Time,Average Content Size,Requests/s,Failures/s,50%,66%,75%,80%,90%,95%,98%,99%,99.9%,99.99%,100%
GET,/items,143,0,3,4.018183930068094,2.0376530000021376,43.720411999970565,4508.188811188811,4.944207919612717,0.0,3,3,3,4,6,7,15,40,44,44,44
POST,/items,25,0,3,6.692363880003995,1.7286199999944074,43.728340999962256,35.0,0.8643720139183071,0.0,3,3,5,6,10,43,44,44,44,44,44
$ head -3 result/run1_stats_history.csv
Timestamp,User Count,Type,Name,Requests/s,Failures/s,50%,66%,75%,80%,90%,95%,98%,99%,99.9%,99.99%,100%,Total Request Count,...
1793358726,0,,Aggregated,0.000000,0.000000,N/A,N/A,N/A,...
1793358727,5,,Aggregated,0.000000,0.000000,42,43,44,44,100,100,100,...
_stats_history.csv は既定で Aggregated 行だけが1秒ごとに記録されます。エンドポイント別の推移が要るときは --csv-full-history を足します。Timestamp は UNIX 時刻(秒)です。
locust.conf で条件を固定する
オプションは locust.conf(または pyproject.toml の [tool.locust])に書けます。キーは長いオプション名と同じです。
# locust.conf
locustfile = locustfile.py
host = http://127.0.0.1:8000
headless = true
users = 10
spawn-rate = 2
run-time = 10s
csv = result/conf
html = result/conf_{u}u_{r}r_{t}.html
only-summary = true
$ locust --config locust.conf
[2026-10-30 20:20:47,786] vm/INFO/locust.main: Run time limit set to 10 seconds
[2026-10-30 20:20:47,787] vm/INFO/locust.runners: Ramping to 10 users at a rate of 2.00 per second
$ ls result/ | grep conf
conf_10u_2r_10.html # {u}{r}{t} が人数・速さ・時間に置き換わる
conf_stats.csv ...
優先順位は「pyproject.toml → ./locust.conf → --config のファイル → 環境変数(LOCUST_USERS など) → コマンドライン」で、後のものが勝ちます。カレントディレクトリの locust.conf は指定しなくても読まれます。手元でも、別の locustfile を -f で指定した実行に locust.conf の html = が効いて、意図しない HTML が増えました。
CI で合否を終了コードにする
既定では「失敗が1件でもあれば終了コード 1」です。「失敗率 1% 以内かつ p95 が 50ms 以内」のような基準で判定したいときは、quitting イベントで process_exit_code を決めます(公式ドキュメントの例を元にした手元のコード)。
# locust_ci.py(抜粋)
from locust import events
@events.quitting.add_listener
def check_thresholds(environment, **kwargs):
stats = environment.runner.stats.total
if stats.fail_ratio > 0.01:
print("NG: failure ratio over 1%")
environment.process_exit_code = 1
elif stats.get_response_time_percentile(0.95) > 50:
print("NG: 95th percentile over 50 ms")
environment.process_exit_code = 1
else:
print("OK: thresholds met")
environment.process_exit_code = 0
$ locust -f locust_ci.py --headless -u 100 -r 100 -t 10s --only-summary; echo "exit=$?"
NG: 95th percentile over 50 ms
[2026-10-30 20:22:00,238] vm/INFO/locust.main: Shutting down (exit code 1)
Aggregated 16026 0(0.00%) | 45 19 187 41 | 1590.25 0.00
exit=1
失敗 0 件でも p95 が 82ms だったため終了コード 1 になり、CI ならこのジョブは落ちます。性能要件(p95 を何 ms にするか)の決め方と合格基準の書き方は、関連記事「性能テストの進め方|性能要件の決め方と合格基準のテンプレート」で扱っています。
よくあるエラー・つまずき(原文と直し方)
この節では、Locust を動かし始めたときに出やすいメッセージと、その直し方が分かります。
次の表は、すべてこの環境で実際に出した原文です。
| 原文 | 原因 | 直し方 |
|---|---|---|
Could not find 'locustfile.py'. Ensure your locustfile ends with '.py' or is a directory with locustfiles. |
カレントディレクトリに locustfile.py が無い | -f ファイル名 を付けるか、そのディレクトリで実行する |
locust.exception.StopTest: You must specify the base host. Either in the host attribute in the User class, or on the command line using the --host option. |
対象 URL をどこにも書いていない | クラスに host = を書くか -H を付ける。headless だとこの原文のあと止まる |
ERROR/locust.user.task: No tasks defined on U. Use the @task decorator or set the 'tasks' attribute of the User (or mark it as abstract = True if you only intend to subclass it) |
@task を付け忘れた |
メソッドに @task を付ける。土台にするだけのクラスは abstract = True |
OSError: [Errno 98] Address already in use: ('127.0.0.1', 8089) |
前の Locust が 8089 番を使ったまま | 前のプロセスを止めるか、--web-port 8090 で別のポートにする。終了コードは 2 |
ConnectionRefusedError(111, 'Connection refused') |
対象が起動していない・ポート違い | 先に curl で対象に届くか確かめる |
ValueError: I/O operation on closed file.(stats_writer の Traceback、終了時) |
終了のタイミングで CSV 書き込みの greenlet が閉じたファイルに触った | 手元では 10 回超の実行のうち1回だけ出て、終了コードは 0・CSV も揃っていた。繰り返すなら --stop-timeout を付けて再現を確かめる |
平均だけで判断する
Avg と Med(中央値)が離れていたら分布が偏っている合図
Locust 側の限界を対象の限界と取り違える
手元では FastHttpUser に替えるだけで RPS が 1.7 倍になった
Locust・k6・JMeter・ab・wrk の判断表(どれを使うか)
この節では、代表的な負荷テストツールの違いと、場面ごとにどれを選ぶかが分かります。
「シナリオをどの言語で書きたいか」と「GUI が要るか」で決まります。 事実関係は各ツールの公式サイトで確かめた範囲です。
| ツール | シナリオの書き方 | 画面 | 向く場面 |
|---|---|---|---|
| Locust | Python のクラス | Web UI(ブラウザ)と headless | ログイン→操作のような流れを Python で書きたい。Python のチーム |
| k6(Grafana Labs) | JavaScript/TypeScript | headless 中心(結果は Grafana などへ) | JS のチーム。しきい値(thresholds)を宣言的に書いて CI で回す |
| JMeter(Apache) | GUI で組む(JMX ファイル)。Java | GUI と CLI | HTTP 以外(JDBC・JMS など)も含む大がかりなテスト計画 |
| ab(Apache Bench) | コマンドの引数(1 URL) | CLI のみ | 1つの URL に N 回・同時 C で叩く簡易な計測 |
| wrk | コマンドの引数+Lua | CLI のみ | 少ない CPU で高い RPS を出す。1 URL の限界測定 |
| 場面 | 選ぶもの | 理由 |
|---|---|---|
| API 1本の限界 RPS を手早く知りたい | ab・wrk | シナリオを書かずに1行で測れる |
| ログインして一覧→詳細→登録、を何百人で | Locust・k6 | 状態(トークン・Cookie)を持つ流れをコードで書ける |
| チームの言語が Python | Locust | 既存のライブラリ・テストコードを流用できる |
| チームの言語が JavaScript | k6 | 同じ言語でシナリオとしきい値を書ける |
| GUI で組みたい・DB や MQ も対象 | JMeter | プロトコルの対応が広い |
| 走らせながら人数を変えて様子を見たい | Locust | Web UI の EDIT で実行中に変えられる |
負荷テストで気をつけること(自分のサーバーだけに向ける)
この節では、Locust を走らせる前に確かめることと、やってはいけないことが分かります。
負荷テストは、見た目は「大量のアクセス」そのものです。向ける先は、自分が管理していて、負荷をかける許可があるサーバーだけです。 他人のサイト・共用のステージング・SaaS の API に向けると、相手のサービスを止めたり、利用規約に反したりします。
- 1対象は自分が管理するサーバーだけhost を 127.0.0.1 や自分のテスト環境にする。本番に向けるときは関係者の合意と時間帯を決める
- 2小さく始める(-u 1 → 10 → 100)locustfile の誤りで本番データが増えることがある。手元の create_item も POST で登録している
- 3外部サービス・メール送信・決済はモックにする対象アプリが外部 API を呼ぶなら、負荷が外部に伝わる
- 4負荷をかける側の CPU を見る1プロセス≒1コア。CPU の警告が出たら --processes か FastHttpUser
- 5Web UI を外に公開しない既定は全アドレスで待ち受け。--web-host 127.0.0.1 か --web-login を使う
- 6結果は CSV と条件(人数・時間・版)をセットで残す--csv と --html、locust.conf を一緒に保存する
負荷の受け手が落ちたときに返るべき 503 や、呼び出し側の再試行・サーキットブレーカーの設計は、負荷テストの結果を受けて見直すところです。既存の 503・サーキットブレーカーの記事で扱っています。
よくある質問
Q. Locust とは何ですか?
A. Python でシナリオを書くオープンソースの負荷テストツールです。仮想ユーザーの行動を HttpUser のクラスで書き、指定した人数ぶん同時に動かして、応答時間・RPS・失敗率を集計します。MIT ライセンスで、pip install locust で入ります。
Q. Locust の読み方は? A. 「ローカスト」です。群れで飛ぶバッタの意味で、仮想ユーザーの群れがサーバーに押し寄せる様子からの名前です。
Q. locust 使い方の最短手順は?
A. pip install locust → HttpUser を継承したクラスに @task のメソッドを書いた locustfile.py を作る → locust --headless -u 10 -r 2 -t 30s -H http://対象。画面で見たいときは locust だけで起動して http://localhost:8089 を開きます。
Q. Number of users と Ramp up(spawn rate)の違いは? A. Number of users は「最大で何人にするか」、Ramp up は「毎秒何人ずつ増やすか」です。100 人・毎秒 10 人なら 10 秒かけて 100 人になります。いきなり最大にしたいときは両方を同じ値にします。
Q. RPS が上がりません。サーバーの限界ですか?
A. 2つの可能性があります。対象サーバーが飽和している(応答時間が伸びている)か、Locust 側の CPU が足りない(ログに CPU の警告が出る)かです。FastHttpUser に替える・--processes で増やす・別の機械から負荷をかける、で同じ天井なら対象の限界です。
Q. 平均応答時間が速いのに利用者が遅いと言います。 A. 平均は多数の速い応答に隠されます。p95・p99 を見ると「20人に1人」「100人に1人」が体験している遅さが分かります。手元でも平均 24ms に対して p95 は 200ms でした。
Q. URL にID が入るので統計の行が増えすぎます。
A. self.client.get(f"/items/{id}", name="/items/[id]") のように name= を付けると1行にまとまります。
Q. ログインが要る API はどう書きますか?
A. on_start でログインしてトークンを受け取り、self.client.headers["Authorization"] に入れます。HttpUser の client は requests.Session なので、Cookie も自動で保たれます。
Q. 200 が返っても失敗にしたいときは?
A. with self.client.get(url, catch_response=True) as res: の中で条件を見て res.failure("理由") を呼びます。遅すぎる・本文が想定と違う、を失敗に数えられます。
Q. CI で合否を判定できますか?
A. できます。既定では失敗が1件でもあれば終了コード 1 です。失敗率や p95 で判定したいときは quitting イベントで environment.process_exit_code を設定します。
Q. Windows でも動きますか?
A. Locust 本体は動きます。ただし --processes は fork() を使うため Windows では使えず、master と worker を別々に起動します(公式ドキュメント)。
Q. k6 と Locust はどちらがよいですか? A. シナリオを Python で書きたいなら Locust、JavaScript なら k6 です。k6 はしきい値を宣言的に書いて CI で回す用途に強く、Locust は Web UI で実行中に人数を変えられる点と、Python のライブラリをそのまま使える点が強みです。
まとめ
- Locust は、仮想ユーザーの行動を Python のクラス(HttpUser・@task・wait_time・on_start)で書く負荷テストツール。
pip install locustで入り、headless と Web UI の両方で動く。 - 結果は「失敗率 → p95/p99 → RPS → 平均」の順に見る。手元では Aggregated の平均 24ms に対して p95 が 200ms で、エンドポイント別に見ないと意味が取れなかった。
- ユーザー数を 50→100 にしても RPS は 1,026→1,084 とほぼ変わらず、p50 が 31→59ms に伸びた。これが「限界に達した」形。
- 失敗の原文は ConnectionRefusedError(対象が起動していない)・ReadTimeout(timeout 秒以内に返らない)・HTTPError 404/500(URL 違い・サーバー内部エラー)。Error report・FAILURES タブ・_failures.csv に同じ文言で出る。
- RPS が伸びないときは Locust 側の CPU も疑う。FastHttpUser で 1.7 倍、
--processes 2で 1.3 倍になった。負荷は自分が管理するサーバーにだけ向ける。
参考・出典
いずれも 2026年10月29日に確認しました。
- Locust「Your first test」(Quickstart):https://docs.locust.io/en/stable/quickstart.html
- Locust「Writing a locustfile」:https://docs.locust.io/en/stable/writing-a-locustfile.html
- Locust「Running without the web UI」:https://docs.locust.io/en/stable/running-without-web-ui.html
- Locust「Distributed load generation」:https://docs.locust.io/en/stable/running-distributed.html
- Locust「Increase performance with a faster HTTP client」:https://docs.locust.io/en/stable/increase-performance.html
- Locust「Configuration」:https://docs.locust.io/en/stable/configuration.html
- Locust「What is Locust?」:https://docs.locust.io/en/stable/what-is-locust.html
- PyPI「locust」:https://pypi.org/project/locust/
- Grafana k6 ドキュメント:https://grafana.com/docs/k6/latest/
- Apache JMeter:https://jmeter.apache.org/
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Locustとは・使い方|Pythonで書く負荷テストを実行結果と失敗の原文で解説」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。