
Lambda Web FunctionsでCold Startがどの程度の間隔で起きるのか計測してみた
こんばんは、情報システム室の夏目です。
Lambda Web FunctionsでもEvnet Function (従来のLambda)と同様にCold Startがあります。
Web FunctionsではCold Startで立ち上がった実行環境が動き続けている間はメモリ量の課金が発生します。
(CPUは実際に動いていた時間だけ)
なので、Cold Startがどの程度の間隔で起きるのか、実行環境がどの程度で落ちるのか計測してみました。
1. 一定間隔でHTTPリクエストを投げてCold Startかどうかを見る
まず最初に、Web Functionに一定間隔でリクエストを投げてCold Startかどうかを記録して集計するということを行いました。
今回は 5分, 10分, 15分, 30分, 60分, 120分, 180分 の7つの間隔でHTTPリクエストを投げました。
計測のために同一のコードのWeb Functionsを7つ作成し、それぞれの間隔でリクエストを投げて記録を行いました。
1-1. 計測のために作成したWeb Function
Web Functionのコードはこちらになります。
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
import { serve } from "@hono/node-server";
import { Hono } from "hono";
let dtColdStart: string | null = null;
let client: DynamoDBDocumentClient | null = null;
const flagDevelopment = process.env.NODE_ENV === "development";
const app = new Hono();
async function putItem(payload: any) {
if (flagDevelopment) {
return;
}
if (client == null) {
client = DynamoDBDocumentClient.from(new DynamoDBClient());
}
await client.send(
new PutCommand({
TableName: process.env.TABLE_NAME,
Item: payload,
}),
);
}
app.get("/", async (c) => {
const sender = c.req.header()["x-sender"] ?? null;
const dtNow = new Date().toISOString();
const payload = {
id: `${process.env.INTERVAL_TYPE}-${crypto.randomUUID()}`,
isColdStart: dtColdStart == null,
dtNow,
dtColdStart: dtColdStart ?? dtNow,
};
if (dtColdStart == null) {
dtColdStart = dtNow;
}
await putItem(payload);
return c.json(payload);
});
if (!flagDevelopment) {
const address = process.env.AWS_LAMBDA_HTTP_ENDPOINT ?? "0.0.0.0:3000";
const separator = address.lastIndexOf(":");
console.log(`http://${address}/`);
serve({
fetch: app.fetch,
hostname: address.slice(0, separator),
port: Number(address.slice(separator + 1)),
});
}
export default app;
今回作成したコードでは、グローバル変数 dtColdStart でCold Startかどうかを区別する形式にしています。
最初のリクエスト、すなわちCold Startのときは dtColdStart = null で、最初のリクエストの終わりに dtColdStart = new Date().toISOString() を代入するようにしています。
また、この記録はDynamoDBに以下の形式で記録しています。
{
// DynamoDBのパーティションキー。頭の三桁の数字がリクエスト間隔 (分)
"id": "005-dd8f1e4c-d045-48bc-b535-442808cc0972",
// ColdStartか否か
"isColdStart": true,
// HTTPリクエストが来た日時
"dtNow": "2026-10-13T13:35:28.463Z",
// Cold Startの日時
"dtColdStart": "2026-10-13T13:35:28.463Z"
}
一定期間後にDynamoDBからデータを取り出して、集計を行います。
その他コード類
{
"name": "trial-measure-web-function-code",
"version": "1.0.0",
"description": "",
"keywords": [],
"type": "module",
"main": "index.js",
"scripts": {
"dev": "vite",
"build": "vite build",
"format": "oxfmt",
"deploy": "vite build && ./deploy.sh"
},
"dependencies": {
"@aws-sdk/client-dynamodb": "^3.1150.0",
"@aws-sdk/lib-dynamodb": "^3.1150.0",
"@hono/node-server": "^2.1.5",
"hono": "^4.13.13"
},
"devDependencies": {
"@hono/vite-build": "^1.11.1",
"@hono/vite-dev-server": "^0.26.1",
"@types/node": "^26.6.5",
"oxfmt": "^0.72.0",
"typescript": "^7.0.2",
"vite": "^8.3.4"
}
}
{
"compilerOptions": {
"target": "es2024",
"lib": ["es2024"],
"types": ["node"],
"module": "preserve",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true
},
"include": ["./src"]
}
import { builtinModules } from "node:module";
import devServer from "@hono/vite-dev-server";
import { defineConfig } from "vite";
export default defineConfig(({ command }) => ({
plugins: [devServer()],
ssr: {
// 依存のバンドルはビルド時のみ。dev で有効にすると CJS パッケージが ESM として評価され `require is not defined` になる
noExternal: command === "build" ? true : undefined,
target: "node",
},
build: {
ssr: "src/index.ts",
outDir: "dist",
emptyOutDir: false,
minify: "oxc",
rollupOptions: {
external: [...builtinModules, /^node:/],
output: { entryFileNames: "index.js" },
},
},
}));
#!/usr/bin/env bash
set -xeuo pipefail
all_intervals=(
"005"
"010"
"015"
"030"
"060"
"120"
"180"
)
for interval in "${all_intervals[@]}"; do
aws lambda-web deploy \
--create \
--name measure-$interval \
--code ./dist \
--execution-role-arn $EXECUTION_ROLE_ARN \
--env INTERVAL_TYPE=$interval \
--env TABLE_NAME=$TABLE_NAME
done
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Statement1",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem"
],
"Resource": [
"*"
]
}
]
}
1-2. 計測結果
半日強、動かした結果は次のようになりました。
| 間隔 | 全体回数 | ColdStart回数 | WarmStart回数 |
|---|---|---|---|
| 5分 | 165 | 60 | 105 |
| 10分 | 82 | 82 | 0 |
| 15分 | 54 | 54 | 0 |
| 30分 | 27 | 27 | 0 |
| 60分 | 13 | 13 | 0 |
| 120分 | 7 | 7 | 0 |
| 180分 | 5 | 5 | 0 |
10分以上の間隔で確実にCold Startになっているようでした。
更に、5分間隔の場合のデータをもう少し詳しく分析してみます。
各Cold Startで何回Warm Startが発生したかを見るために、 dtColdStart の値でグルーピングを行い、各グループの個数で 最大/最小/平均/中央値 を算出してみます。
(1個ならCold Startのみ、 2個ならCold StartとWarm Start 1回、4個ならCold StartとWarm Start 3回)
| グループ数 | 最大 | 最小 | 平均 | 中央値 |
|---|---|---|---|---|
| 60 | 77 | 1 | 2.75 | 1.0 |
中央値が 1.0 なので半数以上はCold Startのようです。
ちなみにグループの個数の内訳は次のようになりました。
| グループ内の個数 | グループ数 |
|---|---|
| 1 | 45 |
| 2 | 6 |
| 3 | 3 |
| 4 | 6 |
| 77 | 1 |
一個だけ妙にWarm Startを繰り返したCold Startがあったようでした。
2. Web Functionのログを確認して、いつ実行環境が落ちるか計測する
Web Functionsでは実行環境を終了させるとき、 SIGTERM が送信され1秒間クリーンアップに使うことができます。
また、Web Functionsは実行環境の初期化やシャットダウンする際にシステムログが記録されます。
なので、今度は実行環境が作成されてからシャットダウンされるまでの時間をログから計測してみます。
やり方は、Web Functionを作成し毎回Cold Startになる10分おきにHTTPリクエストを受けさせます。
また、システムログだけでなく自分でログ出力もさせ、システムログと比較してみます。
2-1. 計測のために作成したWeb Function
今回のコードは次のようになります。
import { serve, type ServerType } from "@hono/node-server";
import { Hono } from "hono";
const FLAG_DEVELOPMENT = process.env.NODE_ENV === "development";
const app = new Hono();
const dtStart = new Date().getTime();
const listRequest: number[] = [];
let server: ServerType | null = null;
app.get("/", (c) => {
listRequest.push(new Date().getTime());
console.warn({ message: "request", req: c.req });
return c.json({ ok: true });
});
if (!FLAG_DEVELOPMENT) {
const address = process.env.AWS_LAMBDA_HTTP_ENDPOINT ?? "0.0.0.0:3000";
const separator = address.lastIndexOf(":");
server = serve({
fetch: app.fetch,
hostname: address.slice(0, separator),
port: Number(address.slice(separator + 1)),
});
}
process.on("SIGTERM", () => {
const payload = {
message: "shutdown",
identifier: "cabede4c-3411-46b1-b2a5-cf4cb9d34cdd",
start: dtStart,
end: new Date().getTime(),
requests: listRequest,
};
console.warn(payload);
if (server) {
server.close((error) => {
process.exitCode = error ? 1 : 0;
});
}
});
export default app;
SIGTERM を検知したときにいつ終了したのかを自分でログ出力を行います。
グローバル変数 dtStart でコードが読み込まれたときの日時を記録しています。
また、HTTPリクエストが来たときの日時もログ出力するようにしています (listRequest)。
その他コード類
{
"name": "trial-measure-sigterm",
"version": "1.0.0",
"description": "",
"keywords": [],
"type": "module",
"main": "index.js",
"scripts": {
"dev": "vite",
"build": "vite build",
"format": "oxfmt"
},
"dependencies": {
"@hono/node-server": "^2.1.5",
"@hono/vite-build": "^1.11.1"
},
"devDependencies": {
"@hono/vite-dev-server": "^0.26.1",
"@types/node": "^26.6.5",
"oxfmt": "^0.73.0",
"typescript": "^7.0.2",
"vite": "^8.3.4"
}
}
{
"compilerOptions": {
"target": "es2024",
"lib": ["es2024"],
"types": ["node"],
"module": "preserve",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true
},
"include": ["./src"]
}
import { builtinModules } from "node:module";
import devServer from "@hono/vite-dev-server";
import { defineConfig } from "vite";
export default defineConfig(({ command }) => ({
plugins: [devServer()],
ssr: {
// 依存のバンドルはビルド時のみ。devで有効にするとCJSパッケージがESMとして評価され `require is not defined` になる
noExternal: command === "build" ? true : undefined,
target: "node",
},
build: {
ssr: "src/index.ts",
outDir: "dist",
emptyOutDir: false,
minify: "oxc",
rolldownOptions: {
external: [...builtinModules, /^node:/],
output: { entryFileNames: "index.js" },
},
},
}));
2-2. 計測結果
今回も半日以上計測を行いました。
調子に乗って17並列にしてしまったので、データ件数は1930件になりました。
(いくつか不備のあるデータがあったので、それを排除して1930件です)
今回の集計は次の項目に対して、 最大/最小/平均/中央値 算出しました。
Duration (System)- システムログから算出した実行環境の稼働時間 (秒)
- 環境の初期化が開始された
platform.initStartから 実行環境のシャットダウンレポートのplatform.spinDownReportまでの時間
Duration (mine)SIGTERMを検知したときにログ出力を行って記録した開始日時と終了日時から算出した実行環境の稼働時間 (秒)
Diff Duration (value)Duration (System)とDuration (mine)の差 (秒)Duration (System) - Duration (mine)で算出
Diff Duration (rate)Duration (System)とDuration (mine)の割合 (%)Duration (mine) / Duration (System)で算出
Duration Last AccessSIGTERMを検知したときにログ出力を行って記録した終了日時とHTTPリクエストの日時から算出した、最終アクセスから実行環境がシャットダウンするまでの時間 (秒)
算出結果は次のようになりました。
| Duration (System) | Duration (mine) | Diff Duration (value) | Diff Duration (rate) | Duration Last Access | |
|---|---|---|---|---|---|
| 最大 | 418.149 | 417.905 | 0.820 | 99.963 | 417.882 |
| 最小 | 274.343 | 274.089 | 0.146 | 99.769 | 274.064 |
| 平均 | 365.683 | 365.464 | 0.219 | 99.940 | 365.442 |
| 中央値 | 366.134 | 365.906 | 0.214 | 99.940 | 365.883 |
実測で調べてみると、だいたい6分くらいで実行環境が落ちるようです。
Duration (mine) をヒストグラムにしたら次のようになりました。

ヒストグラムを生成するコード
import json
import matplotlib.pyplot as plt
import numpy as np
with open("duration_mine.json") as f:
values = json.load(f)
# 階級幅(秒)
bin_width = 2
# データの範囲から階級を自動生成
start = np.floor(min(values) / bin_width) * bin_width
end = np.ceil(max(values) / bin_width) * bin_width
bins = np.arange(start, end + bin_width, bin_width)
fig, ax = plt.subplots(figsize=(12, 5))
ax.hist(
values,
bins=bins,
edgecolor="white",
linewidth=0.5,
)
ax.set_xlabel("Duration (seconds)")
ax.set_ylabel("Frequency")
ax.set_title(f"Duration (mine) [n={len(values):,}]")
ax.grid(axis="y", alpha=0.3)
fig.tight_layout()
fig.savefig("histogram.png", dpi=300)
plt.show()
3. まとめ
二つの方法で実行環境の稼働時間を調べてみました。
5分間隔でリクエストをしたときには大部分がCold Startになったのに、10分間隔でリクエストを投げてログから時間を算出したときには6分程度で実行環境がシャットダウンされていました。
正直、相反する結果になってしまったので困った感じです。
実測の方を信じて6分程度実行環境が動き続けると考えることにします。
何かのお役に立てたら幸いです。









