- 技術 💻
負荷テストの進め方とk6の使い方|スクリプト例と結果の読み方
k6とは、JavaScript(またはTypeScript)でシナリオを書き、Webサービスに負荷をかけて性能を測るオープンソースの負荷テストツールです。Grafana Labsが開発しており、コマンド1つで実行でき、「95%のリクエストが500ms以内」のような合格基準をコードで決められるのが特長です。
この記事では、k6のコマンドや書き方だけでなく、「どの種類の負荷テストを、どの順番で、どう判定するか」という負荷テストの進め方までをまとめます。入門記事ではあまり触れられない、k6 v2系での変更点(--no-summary の廃止など)、scenarios と executor の選び方、結果の数値の読み方、他ツールとの比較表も載せています。スクリプトは公式ドキュメント(grafana.com/docs/k6)の仕様に沿って書いています。
負荷テストとk6の早見表
| 知りたいこと | 答え |
|---|---|
| k6のインストール(macOS) | brew install k6 |
| k6のインストール(Windows) | winget install k6 --source winget |
| スクリプトのひな形を作る | k6 new script.js |
| 実行する | k6 run script.js |
| 仮想ユーザー10人で30秒 | k6 run --vus 10 --duration 30s script.js |
| 合格基準を決める | options.thresholds(例:http_req_duration: ['p(95)<500']) |
| レスポンスの中身を確かめる | check(res, { 'status is 200': (r) => r.status === 200 }) |
| 段階的に負荷を上げる | options.stages または scenarios の ramping-vus |
| 「1秒あたり○件」で負荷をかける | scenarios の constant-arrival-rate |
| 最初に見る数値 | http_req_duration の p(95) と http_req_failed |
| 最新のメジャーバージョン | k6 v2系(2026年9月時点の公式ドキュメントは v2.3.x) |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「負荷テストの進め方とk6の使い方|スクリプト例と結果の読み方」
負荷テストの種類と使い分け
負荷テストは「とりあえずたくさんアクセスする」ものではありません。知りたいことに合わせて、負荷のかけ方を変えます。k6の公式ドキュメントでは、次の種類に分けて説明しています。
| 種類 | 負荷の大きさ | 時間の目安 | 確かめること |
|---|---|---|---|
| スモークテスト | ごく小さい(1〜数VU) | 数秒〜数分 | スクリプトが正しく動くか、最小限の負荷で問題がないか |
| 平均負荷テスト(ロードテスト) | 普段の本番と同じくらい | 5〜60分 | 普段の利用量で、目標の応答速度を保てるか |
| ストレステスト | 普段より高い | 5〜60分 | ピーク時や想定以上の負荷で、どう振る舞うか |
| スパイクテスト | 急激にとても高い | 数分 | 急なアクセス集中に耐えられるか、落ちた後に戻るか |
| 耐久テスト(ソークテスト) | 普段と同じくらい | 数時間 | 長時間動かしたときのメモリ増加や性能の劣化 |
| ブレークポイントテスト | 少しずつ上げ続ける | 限界まで | どこが性能の上限か |
※VU(Virtual User)は仮想ユーザーのことです。1VUが1人の利用者のようにスクリプトを繰り返し実行します。
どの順番で進めるか
スモークテストから始めて、段階的に負荷を上げるのが基本です。
- スモークテスト:スクリプトの間違いや、テスト環境の設定ミスを先に見つける
- 平均負荷テスト:普段の利用量で目標を満たすか確認する(ここが基準値になる)
- ストレス/スパイクテスト:ピーク時やキャンペーン時を想定して確認する
- 耐久テスト:長時間の運用で問題が出ないか確認する
負荷のかけ方は「増やす → 一定に保つ → 減らす」のシンプルな形にすると、結果を比べやすくなります。複雑に上下させると、どこで遅くなったのかが分かりにくくなります。
k6のインストール
公式ドキュメントに記載されている方法です。
# macOS
brew install k6
# Windows
winget install k6 --source winget
# Debian / Ubuntu
curl -fsSL https://dl.k6.io/key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/k6-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
# Docker
docker pull grafana/k6
k6本体はGoで作られた単体のプログラムです。スクリプトはJavaScriptで書きますが、Node.jsの上で動くわけではありません。そのため、npmパッケージ(fs や axios など)はそのままでは使えない点に注意します。npmの k6 パッケージはk6本体ではないので、インストールは上記の方法で行います。
k6 v1.0以降は、TypeScriptのファイル(.ts)もそのまま実行できます。
最初のスクリプト
// script.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 10, // 仮想ユーザー10人
duration: '30s', // 30秒間
};
export default function () {
const res = http.get('https://test.k6.io/');
check(res, {
'status is 200': (r) => r.status === 200,
});
sleep(1); // 利用者が画面を見る時間の代わり
}
k6 run script.js
| 部分 | 役割 |
|---|---|
export const options |
負荷のかけ方と合格基準の設定 |
export default function |
各VUが繰り返し実行する処理(1回を「イテレーション」と呼ぶ) |
check |
レスポンスが期待どおりかを確かめる。失敗してもテストは止まらない |
sleep(1) |
次のリクエストまで1秒待つ |
sleep を入れないと、1VUが休みなくリクエストを送り続けます。実際の利用者より強い負荷になるため、「VU10人=利用者10人」という対応が崩れます。利用者の動きを再現したいときは sleep を入れます。
https://test.k6.io/ は、k6の公式が練習用に公開しているサイトです。自分が管理していないサイトには負荷をかけないでください(後述)。
options:負荷のかけ方を決める
stages:段階的に負荷を上げ下げする
export const options = {
stages: [
{ duration: '2m', target: 50 }, // 2分かけて50VUまで増やす
{ duration: '5m', target: 50 }, // 50VUを5分維持
{ duration: '1m', target: 0 }, // 1分かけて0まで減らす
],
};
これが平均負荷テストの基本形です。target を普段の利用量より大きくすればストレステスト、短い duration で一気に上げればスパイクテストになります。
| テストの種類 | stages の例 |
|---|---|
| スモーク | vus: 1, duration: '1m' |
| 平均負荷 | 2分で50 → 5分維持 → 1分で0 |
| ストレス | 5分で200 → 10分維持 → 2分で0 |
| スパイク | 30秒で500 → 1分維持 → 30秒で0 |
| 耐久 | 5分で50 → 4時間維持 → 5分で0 |
VUの数は例です。普段の利用量(アクセスログや監視ツールの値)から逆算して決めます。
thresholds:合格・不合格の基準を決める
thresholds(しきい値)は、テストの合否をコードで決める仕組みです。基準を満たさないと、k6は0以外の終了コードで終わるため、CIでそのまま「失敗」として扱えます。
export const options = {
thresholds: {
http_req_failed: ['rate<0.01'], // エラー率 1% 未満
http_req_duration: ['p(95)<500'], // 95%のリクエストが500ms未満
checks: ['rate>0.99'], // check の成功率 99% 超
},
};
明らかに基準を超えたら途中で止めることもできます。
export const options = {
thresholds: {
http_req_failed: [
{ threshold: 'rate<0.05', abortOnFail: true, delayAbortEval: '30s' },
],
},
};
| 項目 | 意味 |
|---|---|
abortOnFail: true |
基準を超えた時点でテストを止める |
delayAbortEval: '30s' |
開始直後のばらつきで止まらないよう、判定を30秒遅らせる |
エンドポイントごとに基準を変えるには、リクエストにタグを付けます。
export const options = {
thresholds: {
'http_req_duration{name:list}': ['p(95)<300'],
'http_req_duration{name:search}': ['p(95)<800'],
},
};
export default function () {
http.get('https://example.com/api/items', { tags: { name: 'list' } });
http.get('https://example.com/api/search?q=abc', { tags: { name: 'search' } });
}
checks:レスポンスの中身を確かめる
check はレスポンスの検証、thresholds は全体の合否、という役割分担です。
const res = http.get('https://example.com/api/items');
check(res, {
'status is 200': (r) => r.status === 200,
'has items': (r) => r.json('items').length > 0,
'fast enough': (r) => r.timings.duration < 500,
});
check は失敗してもテストを止めません。失敗の割合で合否を判定したいときは、上のように thresholds に checks: ['rate>0.99'] を書きます。
「ステータスが200でも、中身がエラー画面だった」ということは実際によくあります。ステータスコードだけでなく、中身も check すると、見かけだけ速い結果にだまされにくくなります。
scenarios:本番に近い負荷を組み立てる
scenarios を使うと、複数の負荷パターンを1つのテストで同時に動かせます。各シナリオには「executor(実行方式)」を指定します。
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
scenarios: {
browse: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 40 },
{ duration: '5m', target: 40 },
{ duration: '1m', target: 0 },
],
exec: 'browse',
},
api_orders: {
executor: 'constant-arrival-rate',
rate: 20, // 1秒あたり20イテレーション
timeUnit: '1s',
duration: '8m',
preAllocatedVUs: 20,
maxVUs: 100,
exec: 'order',
},
},
thresholds: {
'http_req_duration{scenario:browse}': ['p(95)<500'],
'http_req_duration{scenario:api_orders}': ['p(95)<800'],
http_req_failed: ['rate<0.01'],
},
};
export function browse() {
const res = http.get('https://example.com/');
check(res, { 'top 200': (r) => r.status === 200 });
sleep(Math.random() * 3 + 1); // 1〜4秒ほど画面を見る
}
export function order() {
const res = http.post(
'https://example.com/api/orders',
JSON.stringify({ itemId: 1, qty: 1 }),
{ headers: { 'Content-Type': 'application/json' } },
);
check(res, { 'order 201': (r) => r.status === 201 });
}
executorの選び方
| executor | 負荷の決め方 | 向いている場面 |
|---|---|---|
shared-iterations |
全VUで合計○回 | 決まった件数を処理するテスト |
per-vu-iterations |
各VUが○回ずつ | データを1件ずつ割り当てるテスト |
constant-vus |
VU数を一定に保つ | シンプルな負荷テスト |
ramping-vus |
VU数を段階的に変える | stages と同じ。平均負荷・ストレス |
constant-arrival-rate |
1秒あたりのイテレーション数を一定に保つ | APIの処理能力(RPS)の確認 |
ramping-arrival-rate |
1秒あたりの数を段階的に変える | ブレークポイントテスト |
「VUで決める」と「到着率で決める」の違い
ここは負荷テストで特に大切な考え方です。
| 方式 | サーバーが遅くなると… |
|---|---|
VUで決める(ramping-vus など) |
各VUが応答を待つため、送るリクエスト数が減る |
到着率で決める(constant-arrival-rate など) |
決めた数のリクエストを送り続ける(足りなければVUを追加) |
VU方式では、サーバーが遅くなるほど負荷が自然に下がるため、限界を実際より高く見積もることがあります。「1秒あたり○件をさばけるか」を確かめたいときは、到着率方式(arrival-rate)を使います。maxVUs まで使い切ると、k6は dropped_iterations(実行できなかったイテレーション)を記録します。この値が出たら、狙った負荷をかけられていないというサインです。
テストのライフサイクル(setup / teardown)
export function setup() {
// テスト開始前に1回だけ実行(ログインしてトークンを取るなど)
const res = http.post('https://example.com/api/login',
JSON.stringify({ user: 'loadtest', password: __ENV.LT_PASSWORD }),
{ headers: { 'Content-Type': 'application/json' } });
return { token: res.json('token') };
}
export default function (data) {
http.get('https://example.com/api/me', {
headers: { Authorization: `Bearer ${data.token}` },
});
}
export function teardown(data) {
// テスト終了後に1回だけ実行(テストデータの後片付けなど)
}
| 段階 | 実行回数 | 用途 |
|---|---|---|
| 初期化(ファイルの一番外側) | VUごとに1回 | import、options、テストデータの読み込み |
setup |
1回 | ログイン、テストデータ作成 |
default |
繰り返し | 測りたい処理 |
teardown |
1回 | 後片付け |
パスワードなどはスクリプトに直接書かず、k6 run -e LT_PASSWORD=... script.js のように環境変数(__ENV)で渡します。
複数ユーザーのテストデータは SharedArray で読み込むと、全VUで1つのデータを共有でき、メモリを節約できます。
import { SharedArray } from 'k6/data';
const users = new SharedArray('users', function () {
return JSON.parse(open('./users.json'));
});
export default function () {
const user = users[__VU % users.length]; // VUごとに別のユーザー
// ...
}
結果の読み方
k6 run が終わると、結果のまとめ(end-of-test summary)が表示されます。k6 v1.0以降は、合否(THRESHOLDS)と全体の結果(TOTAL RESULTS)を中心にした「compact」形式が既定になりました。以下は公式ドキュメントに掲載されている出力形式の例(一部抜粋)です。
█ THRESHOLDS
http_req_duration
✓ 'p(95)<1500' p(95)=148.21ms
█ TOTAL RESULTS
checks_total: 90 13.122179/s
checks_succeeded: 100.00%
HTTP
http_req_duration: avg=140.36ms min=119.08ms
http_reqs: 45 6.56109/s
まず見る指標
| 指標 | 意味 | 見るポイント |
|---|---|---|
http_req_duration |
リクエストを送ってから応答を受け取り終わるまでの時間 | 平均ではなく p(95)・p(99) を見る |
http_req_failed |
失敗したリクエストの割合 | 既定では 4xx・5xx などが失敗扱い |
http_reqs |
リクエストの総数と1秒あたりの数 | 狙ったRPSが出ているか |
checks |
check の成功率 | 中身の検証が通っているか |
iterations |
default 関数の実行回数 | 想定の回数を回せているか |
vus / vus_max |
実行中のVU数 | 狙いどおりに増減したか |
dropped_iterations |
実行できなかったイテレーション | 出ていたら負荷が足りていない |
http_req_waiting |
サーバーが処理している時間(TTFB) | ここが大きいならサーバー側の処理が遅い |
p(95) を見る理由
p(95) は「95%のリクエストがこの時間以内に終わった」という値です。平均値は、ほとんど速くて一部だけとても遅い場合でも、それなりの数字になってしまいます。
| リクエスト | 応答時間 |
|---|---|
| 96件 | 100ms |
| 4件 | 3,000ms |
| 平均 | 216ms |
| p(95) | 100ms |
| p(99) | 3,000ms |
平均だけを見ると「216msなら十分速い」と判断しがちですが、実際には100人に4人が3秒待っています。この例では p(95) も100msのため、p(99) まで合わせて見ると、遅い利用者がどれくらいいるかがわかります。
結果の判断表
| 結果の様子 | 考えられること | 次に見るもの |
|---|---|---|
| VUを増やしても RPS が増えず、応答時間だけ伸びる | どこかが上限に達している | CPU・DB接続数・スレッド数 |
| エラー率が途中から急に上がる | 接続上限やタイムアウト | サーバーのログ、DBの接続プール |
| 時間とともに応答時間がじわじわ伸びる | メモリリーク、キャッシュ肥大 | 耐久テストでメモリの推移 |
dropped_iterations が出る |
k6側のVUが足りない | maxVUs を増やす、k6マシンの性能 |
| k6マシンのCPUが高い | k6自体が上限 | k6を複数台に分ける、スクリプトを軽くする |
k6を動かしているマシンの CPU やネットワークも必ず確認します。負荷をかける側が上限に達していると、サーバーではなくk6側の限界を測ってしまいます。
結果をファイルや外部に出す
# 全データを JSON(1行1データ)で出力
k6 run --out json=results.json script.js
# まとめを詳しく表示(グループ・シナリオごとの結果も出す)
k6 run --summary-mode=full script.js
# 実行中の様子をブラウザで見る(Webダッシュボード)
K6_WEB_DASHBOARD=true k6 run script.js
handleSummary 関数を書くと、まとめを JSON や HTML など好きな形式でファイルに保存できます。
export function handleSummary(data) {
return {
'summary.json': JSON.stringify(data, null, 2),
};
}
k6 v2系での主な変更点
古い記事の書き方をそのまま使うとエラーになるものがあります。
| 以前の書き方 | v2系での書き方 |
|---|---|
--no-summary |
--summary-mode=disabled |
--summary-mode=legacy |
廃止(compact か full を使う) |
k6 cloud script.js |
k6 cloud run script.js |
options.ext.loadimpact |
options.cloud |
executor: 'externally-controlled' |
廃止(ramping-vus などに置き換える) |
k6/experimental/redis |
k6/x/redis |
ツールの比較:k6・JMeter・Locust・Gatling
| 項目 | k6 | JMeter | Locust | Gatling |
|---|---|---|---|---|
| シナリオの書き方 | JavaScript / TypeScript | GUIで作成(XMLで保存) | Python | Java・Kotlin・Scala(JS/TSのSDKもあり) |
| 実行の軽さ | 1プロセスで多くのVUを動かせる | スレッドごとにメモリを使う | 軽い(並行処理に gevent を使用) | 軽い |
| 合否の判定 | thresholds で標準対応 | アサーション+プラグイン等 | コードで自前実装 | assertions で標準対応 |
| CIとの相性 | 終了コードでそのまま判定できる | CLIモードで実行 | ヘッドレスモードで実行 | Maven/Gradle 等から実行 |
| 画面で操作 | なし(Webダッシュボードで観察) | GUIが充実 | Web UIあり | なし(HTMLレポートあり) |
| 対応プロトコル | HTTP、WebSocket、gRPC、ブラウザ | 非常に幅広い(JDBC、FTP、JMSなど) | HTTP中心(拡張可能) | HTTP、WebSocket、JMS など |
| 向いているチーム | 開発者がコードで管理したい | GUIで作りたい、多様なプロトコルが必要 | Pythonに慣れている | JVM系の開発が中心 |
どれが優れているかではなく、チームの言語と目的で選ぶのがよい考え方です。コードレビューやGitでの管理、CIでの自動判定を重視するならk6が扱いやすく、GUIで試行錯誤したい場合や特殊なプロトコルが必要な場合はJMeterが便利です。
負荷テストの計画と注意点
テスト前に決めておくこと
| 決めること | 例 |
|---|---|
| 目的 | 「新しい検索APIが、ピーク時の2倍に耐えられるか」 |
| 対象 | 検索API、商品一覧ページ(ログイン後) |
| 目標値 | p(95) 500ms 未満、エラー率 1% 未満、100 RPS |
| 負荷の根拠 | 直近のピーク時のアクセス数(監視ツールの値) |
| 環境 | 本番と同じ構成の検証環境 |
| 実施日時と連絡先 | インフラ担当・関係チームへの事前連絡 |
| 止める条件 | エラー率5%超で中止(abortOnFail) |
目標値を先に決めておくと、結果を見て「速い/遅い」で迷わずに済みます。
やってはいけないこと・気をつけること
- 本番環境にいきなり負荷をかけない:利用者に影響が出ます。本番と同じ構成の検証環境で行うのが基本です。やむを得ず本番で行う場合は、関係者の合意、時間帯、止める条件を決めてから実施します
- 自分が管理していないサイトに負荷をかけない:相手のサービスを止めてしまうおそれがあり、攻撃とみなされることもあります
- 外部サービスへの通信に注意する:支払いやメール送信、SMSなど外部APIを呼ぶ処理は、モック(テスト用の偽物)に置き換えるか、テスト用の環境を使います
- クラウドやCDNの設定を確認する:WAFやレート制限でk6の通信がブロックされると、正しい結果になりません。事前に検証環境の設定を確認します
- ネットワーク距離をそろえる:k6を動かす場所とサーバーの距離によって応答時間が変わります。毎回同じ場所から実行します
- テストデータを用意する:同じユーザー・同じ商品ばかりだとキャッシュが効きすぎて、実際より速い結果になります
- 同時にサーバー側も監視する:k6の結果だけでは原因がわかりません。CPU、メモリ、DB、ログを並べて見ます
実行前のチェックリスト
| チェック | 確認 |
|---|---|
| 対象のURLは検証環境になっているか | □ |
| 関係者に日時と内容を連絡したか | □ |
| スモークテストでスクリプトが正しく動いたか | □ |
| thresholds で合格基準を決めたか | □ |
| 止める条件(abortOnFail)を決めたか | □ |
| サーバー側の監視画面を開いているか | □ |
| 外部APIへの通信はモックに置き換えたか | □ |
| k6マシンのCPU・ネットワークに余裕があるか | □ |
CIで定期的に動かす
thresholds を満たさないとk6は0以外の終了コードで終わるため、GitHub Actions などのCIでそのまま合否を判定できます。CIでは短いスモークテストや平均負荷テストを動かし、性能が落ちる変更を早めに見つける使い方が向いています。
k6公式の Docker イメージ(grafana/k6)を使えば、CIのジョブ内でもインストールなしで実行できます。
よくある質問
Q. k6はブラウザの操作も測れますか?
A. はい。k6/browser モジュール(v1.0で正式機能になりました)で、実際のブラウザを動かして画面表示の速さを測れます。ただしブラウザは1VUあたりの負荷が大きいため、大量の負荷はHTTPのテストでかけ、ブラウザのテストは少数で体感速度を測る組み合わせが一般的です。
Q. VUはいくつに設定すればいいですか?
A. 実際の利用量から決めます。「1秒あたりのリクエスト数」が分かっている場合は、VUではなく constant-arrival-rate で RPS を直接指定する方が、狙った負荷を正確にかけられます。
Q. k6はnpmでインストールできますか?
A. npmの k6 パッケージはk6本体ではありません。k6本体は Homebrew、winget、apt、Docker などでインストールします。スクリプトはJavaScriptですが、Node.jsではなくk6の実行環境で動きます。
Q. 平均値が速いのにユーザーから「遅い」と言われます。 A. p(95)・p(99) を確認してください。一部のリクエストだけが極端に遅い場合、平均値には表れにくくなります。タグを付けてエンドポイントごとに分けると、どこが遅いのかを絞り込めます。
まとめ
- k6は JavaScript/TypeScriptでシナリオを書き、合否までコードで決められる負荷テストツール
- 負荷テストは スモーク → 平均負荷 → ストレス/スパイク → 耐久 の順に進める
- 合否は thresholds(p(95)、エラー率、check成功率)で決め、CIでも判定できる
- 「1秒あたり○件」を確かめるなら 到着率方式(constant-arrival-rate)。VU方式は遅くなると負荷が下がる
- 結果は 平均ではなく p(95)・p(99) と
http_req_failed、dropped_iterationsを見る - 本番環境や他人のサイトに負荷をかけない。検証環境・事前連絡・止める条件をそろえる
- v2系では
--no-summaryが--summary-mode=disabledに変わるなど、古い書き方に注意
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「負荷テストの進め方とk6の使い方|スクリプト例と結果の読み方」
株式会社RJCでは、システムの性能を確かめる方法も、プロジェクトの中で仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。