0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Lambda Web Functions で Elysia を動かして、デプロイ時間・コールドスタート・SSE/WebSocket を測ってみた

0
Posted at

Lambda Web Functions は、ポートで待ち受ける HTTP サーバーをそのまま動かせる AWS Lambda の新しい関数タイプで、2026 年 10 月 12 日に一般提供が始まりました。この記事では、Elysia のアプリを Lambda Web Functions にデプロイして、デプロイ時間・コールドスタート・SSE と WebSocket の挙動を測った結果と、Cloudflare Workers や Event Functions との比較を紹介します。

検証は 2026 年 10 月 14 日に東京リージョン(ap-northeast-1)で行いました。AWS CLI 2.37.13 を使用し、ランタイムは nodejs24.x(Node.js v24.21.0)です。また、レイテンシ計測は神戸市内の自宅から行っています。

Lambda Web Functions とは

環境変数 AWS_LAMBDA_HTTP_ENDPOINT で渡されるホストとポートで HTTP サーバーを起動しておくと、Lambda がそのサーバーへリクエストを転送する仕組みです。従来の Lambda 関数(今回 Event Functions に改称)のようなハンドラーは不要で、node index.js で動くサーバーをそのままデプロイできます。1 つの実行環境(2 vCPU / 2 GB)がデフォルトで最大 64 リクエストを同時に処理します。

対応ランタイムは Node.js 24(arm64)のみです。課金はアクティブな CPU 時間と、実行環境が存在している間のメモリ使用量が中心です。

Elysia アプリを作成する

Lambda Web Functions は Node.js のみの対応なので、Elysia の公式 Node.js アダプター @elysia/node を使います。

npm init -y
npm pkg set type=module
npm install elysia @elysia/node
index.js
import { Elysia, sse } from 'elysia'
import { node } from '@elysia/node'
import { randomUUID } from 'node:crypto'
import { setTimeout as sleep } from 'node:timers/promises'

// モジュール読み込み時刻と実行環境ごとの ID(コールドスタートの判定に使う)
const bootedAt = Date.now()
const instanceId = randomUUID().slice(0, 8)
// プロセス起動から listen のコールバックまでの時間
let readyMs

// Lambda Web Functions はこのアドレスに対してヘルスチェックする
const [hostname, port] = (
  process.env.AWS_LAMBDA_HTTP_ENDPOINT ?? '0.0.0.0:3000'
).split(':')

new Elysia({ adapter: node() })
  .get('/', ({ request }) => ({
    instanceId,
    uptimeMs: Date.now() - bootedAt,
    readyMs,
    region: process.env.AWS_REGION,
    requestId: request.headers.get('lambda-web-request-id'),
  }))
  .get('/sleep', async ({ query }) => {
    await sleep(Number(query.ms ?? 1000))
    return { instanceId }
  })
  // /sse と /ws は後述
  .listen({ hostname, port: Number(port) }, (server) => {
    readyMs = Math.round(performance.now())
    console.log(`listening on ${server.hostname}:${server.port} (${readyMs}ms)`)
  })

instanceId が変わればコールドスタートが起きたと判定できます。AWS_LAMBDA_HTTP_ENDPOINT の実際の値は 0.0.0.0:3000 です。Elysia の listen にこの文字列をそのまま渡すと Port must be a numeric value で失敗するため、{ hostname, port } に分けて渡しています。

デプロイ時間を測る

aws lambda-web deploy は、--code のディレクトリを ZIP にして S3 にアップロードし、IAM ロール・関数・dev エンドポイントまでまとめて作成します。npm install は実行しないため、node_modules を用意してから実行します。

aws lambda-web deploy --name elysia-article --code . --create \
  --auth-type ApplicationManaged --region ap-northeast-1
[+0.51s] Creating deployment package ...
[+0.88s] Preparing Amazon S3 bucket ...
[+0.98s] Preparing AWS IAM execution role ...
[+2.08s] Uploading deployment package ...
[+2.27s] Creating Lambda Web Function ...
[+13.50s] Waiting for endpoint to become active (timeout 600s) ...
[+23.96s] https://gp8lqr297jpkchjf.kzr.lambda-web.ap-northeast-1.on.aws

(行頭はコマンド開始からの経過秒数。隠しファイルのスキップ警告と待機中の行は省略)

関数名を変えて新規作成を 5 回行った結果が以下です。

フェーズ 最小 中央値 最大
CreateWebFunction の呼び出し 11.15 秒 11.23 秒 11.58 秒
エンドポイントの Pending 待ち 10.42 秒 10.46 秒 10.49 秒
URL 表示まで 23.57 秒 23.96 秒 25.16 秒
最初の HTTP 200 まで 24.24 秒 24.76 秒 26.14 秒

ほかのフェーズ(パッケージ作成、S3、IAM、アップロード)は合計 1.5 秒程度です。最初の HTTP 200 の uptimeMs は 5 回とも 44〜50 ms で、デプロイ直後に実行環境が先回りして用意されることはなく、最初のアクセスは必ずコールドスタートになります。

--no-wait を付けると CLI は 13.4〜13.8 秒で戻りますが、URL は出力されません(get-web-function-endpoint の domainName で取得)。Active になるまでは HTTP 409(Platform.EndpointNotActive)が返り、最初の 200 は 25.19 秒後でした。短くなるのは CLI の待ち時間だけです。

既存の関数を --create なしで 3 回更新すると、CLI の完了まで 7.54〜7.92 秒でした。250 ms 間隔でアクセスし続けると、新バージョンは 5.66〜8.45 秒後に返り始め、旧バージョンも 10.73〜13.34 秒後まで返り続けました。デプロイ直後の数秒間は新旧が混在する前提でアプリを作るのが安全です。

コールドスタートを測る

初回リクエスト

新規にデプロイした 8 個の関数で、最初のリクエストと直後のリクエストを curl で比べました。毎回接続し直し、TLS ハンドシェイク完了から最初のバイトまでを「サーバー待ち」としています。

最初(コールド) 直後(ウォーム、関数ごとの中央値)
サーバー待ち 487〜709 ms(中央値 644 ms) 15〜29 ms
合計 537〜758 ms(中央値 690 ms) 43〜57 ms

起動時に CloudWatch Logs(/aws/lambda/web/<関数名>)へ出る platform.initReport の totalDurationMs は、166 個の実行環境で中央値 457 ms(最大 533 ms)でした。上乗せ分 0.5〜0.7 秒の大半がこの初期化です。totalCpuTimeMs が所要時間とほぼ同じなので、読み込むモジュールを減らすのが有効そうです。

放置後に戻るまでの時間

縮退までの時間はドキュメントに記載がないため、2〜60 分放置してからアクセスしました。2 分・5 分はウォームのまま(58〜60 ms)、8 分以上はすべてコールドスタート(545〜801 ms)です。13 個の実行環境で、最後のリクエストから停止ログ platform.spinDownStart までは 336〜412 秒でした。最後のリクエストから約 6 分でコールドスタートに戻り、その間のメモリは課金対象です。アクセスが散発的な場合にはメモリが遊んでしまいそうです。

停止時の SIGTERM

platform.spinDownReport は何もしないと status: failure(Runtime.SpinDownTimeout)になります。Lambda は停止時に SIGTERM を送って最大 1 秒待ち、終了しなければ SIGKILL するためです。ハンドラーを追加すると 70 ms で success になりました。

index.js(追記)
process.on('SIGTERM', () => {
  console.log(`SIGTERM received (instance=${instanceId})`)
  process.exit(0)
})

どちらの場合もリクエストへの影響はありませんでしたが、後始末が必要なアプリは 1 秒以内に終了させる必要があります。

同時アクセス

実行環境が 0 の関数に、1 秒待つ /sleep?ms=1000 を別々の TLS 接続で 100 件同時に送りました。比較用に --max-concurrency-per-environment 1 のリビジョンでも計測しています(--code を省略すると設定だけ変えたリビジョンになります)。

上限 64(デフォルト) 上限 1
HTTP ステータス 200 × 100 200 × 94、429 × 6
使われた実行環境 51 94
応答時間 p50 / p95 / 最大 1,681 / 1,755 / 1,930 ms 1,720 / 1,995 / 2,060 ms(200 のみ)

デフォルトでも 1 環境あたり最大 2 件で、コールドスタートは上限 1 の約半分にとどまりました。直後にもう 100 件送ると実行環境は 72 個に増えています。CPU 使用率などで同時実行数を動的に調整しているため、突発的なアクセスでは 64 まで詰め込まずに実行環境を増やすようです(後述の SSE では 20 本が 1 環境に収まりました)。

アカウントの vCPU 上限

429 の lambda-web-error-type は Platform.AccountActiveVCPUArm64Throttle でした。aws lambda-web get-web-account-settings で確認した maxTotalArmVCpus は 200 で、リージョン内の全関数で約 100 環境が上限です。ドキュメントのクォータ表は 2,000 vCPU ですが、新しいアカウントは低い値から始まり、利用状況に応じて自動で引き上げられます。

この上限到達は、同じアカウントの別の関数にも及びました。約 4 分間、新しい実行環境が必要なリクエストは 429 になり、その間に新規デプロイした関数は最初から 429 を返し続けました。負荷試験の前には上限を確認し、--max-environments で関数ごとの実行環境数を制限しておくと安全です。

SSE を試す

Elysia では、ジェネレーター関数から sse() で包んだ値を yield すると SSE としてストリーミングされます。

index.js(抜粋)
  .get('/sse', async function* ({ query }) {
    const count = Number(query.count ?? 5)
    try {
      for (let i = 1; i <= count; i++) {
        yield sse({ event: 'tick', id: String(i), data: { i, sentAt: Date.now(), instanceId } })
        await sleep(1000)
      }
    } finally {
      console.log(`sse finished (instance=${instanceId})`)
    }
  })
$ curl -sN -i "https://7h21g320df3t29v2.kzr.lambda-web.ap-northeast-1.on.aws/sse?count=10"
HTTP/1.1 200 OK
Content-Type: text/event-stream
Transfer-Encoding: chunked
(ほかのヘッダーは省略)

id: 1
event: tick
data: {"i":1,"sentAt":1791951153242,"instanceId":"e0dbdbb0"}
(以下略)

最初のイベントは 97 ms で届き、以降の受信間隔は 997〜1,002 ms で、バッファリングはされません。20 本を同時に開いても全本が完了し、すべて同じ実行環境で処理されました。エンドポイントは HTTP/1.1 のみの対応です。

タイムアウトでエラーなしに切れる

リクエストタイムアウトはデフォルト 30 秒(3〜900 秒)で、ストリーミングではストリーム全体に適用されます。45 件を要求すると 30 件目の直後、30.09 秒で終了しました。ステータスは 200 のまま、チャンク転送も正常に終端し、curl の終了コードも 0 です。クライアントは送信完了とタイムアウトを区別できません。通常のレスポンスなら 30.06 秒で HTTP 504(Runtime.RequestTimeout)になります。

--timeout-seconds 120 を付けてデプロイし直すと、45 件すべて届きました。LLM の応答をストリーミングする用途では、最長の応答時間に合わせて設定しておく必要があります。

切断の検知が 4 秒遅れる

/sse?count=30 を 3 件受け取った時点で中断すると、サーバーはその後も 4〜7 件目を生成し、finally が動いたのは 4.0 秒後でした(2 回とも同じ。ローカルでは約 1 秒)。Lambda のプロキシーを挟むと切断の伝達が数秒遅れるため、高価な LLM 呼び出しを中継する場合は数秒分の余計な処理を見込んでおきましょう。

WebSocket は使えない

Elysia の .ws() でエコーサーバーを追加すると、ローカルでは動きますが、Lambda では wss:// のハンドシェイクに失敗します(コード 1006)。

index.js(抜粋)
  .ws('/ws', {
    open(ws) {
      ws.send({ type: 'hello', instanceId })
    },
    message(ws, message) {
      ws.send({ type: 'echo', message })
    },
  })

curl でハンドシェイクを送ると Elysia の 404 が返りました。アプリに届いたヘッダーを確認すると、Sec-WebSocket-* は残っているものの Upgrade は消え、Connection は close に置き換わっていました。ドキュメントにも「Lambda does not support WebSocket upgrades」と明記されています。双方向通信には API Gateway の WebSocket API を併用するか、配信を SSE、送信を POST に分ける構成になります。

Cloudflare Workers と比べる

同じ Elysia アプリを Cloudflare 向けアダプターで Workers にデプロイし、同じ Mac から計測しました(処理拠点は大阪)。WebSocket には Durable Objects を使っています。料金は 2026 年 10 月 15 日時点の公式ドキュメントと AWS の Price List API の値です。

Lambda Web Functions Cloudflare Workers
実行環境 Node.js 24(2 vCPU / 2 GB) V8 の isolate(128 MB)
実行場所 5 リージョン 330 以上の都市
1 リクエストの上限 経過時間 3〜900 秒 CPU 時間 最大 5 分(経過時間は無制限)
課金 リクエスト、CPU 時間、メモリ × 存続時間 リクエスト、CPU 時間
データ転送料(東京) 1 GB あたり \$0.114 なし
WebSocket 非対応 対応(Durable Objects)
AWS へのアクセス IAM ロール アクセスキーが必要
デプロイ 中央値 23.96 秒 中央値 4.21 秒
起動時間 中央値 457 ms 9〜13 ms
コールドスタート時の応答 中央値 690 ms 中央値 85.7 ms
ウォーム時の応答 43〜57 ms 中央値 36.6 ms

コールドスタートは Workers が約 8 分の 1 で、10 分放置後も既存の isolate が 57 ms で応答しました。一方で Workers の Node.js API は互換レイヤー(nodejs_compat)経由です。今回もグローバルスコープで crypto.randomUUID() がエラーになり、Date.now() が 0 を返したため書き換えが必要でした。Elysia の Workers アダプターは実験的で .ws() も使えません。既存の Node.js サーバーをほぼそのまま動かせる点は Lambda Web Functions の強みです。

また、Lambda Web Functions の Node.js が使える利点を活かそうとすると、Next.js 等のフルスタックなフレームワークを動かしたくなります。その場合、HTML やクライアント JS の転送によってデータ転送料が大きくなる可能性がある点が若干ミスマッチに感じています。

コスト

単価は、Lambda Web Functions(東京)がリクエスト 100 万件 \$0.30、vCPU 秒 \$0.0000322421、GB 秒 \$0.0000042688 です。Workers 有料プランは月額 \$5 で月 1,000 万リクエストと 3,000 万 CPU ミリ秒を含み、超過はリクエスト 100 万件 \$0.30、CPU 100 万ミリ秒 \$0.02 です。実行環境が 1 か月間 1 つ動き続ける前提で、データ転送料を除いて試算しました。

A: AWS 料金ページの試算条件 B: 小規模な API
リクエスト / 月 1,000 万 100 万
CPU 時間 / リクエスト 50 ms 5 ms
メモリ消費 0.29 GB 0.18 GB
Lambda Web Functions \$22.33 \$2.45
Workers(有料プラン) \$14.40 \$5.00
Workers(無料プラン) 利用不可 \$0

B の Lambda Web Functions は \$2.45 のうち \$1.99 がメモリ料金で、リクエストが少なくても実行環境が残る間は課金されます。B は Workers の無料プラン(1 日 10 万リクエスト、CPU 10 ms)に収まります。

WebSocket

Workers と Durable Objects のエコーサーバーは、ハンドシェイク 122〜123 ms(初回 533 ms)、往復 12.9〜15.4 ms でした。WebSocket Hibernation API を使えば、メッセージのない間は実行時間が課金されません。1,000 クライアントが 1 か月つなぎっぱなしで、各自 1 分に 1 件送受信するチャットを試算しました。

API Gateway WebSocket API(東京) Durable Objects
接続時間 \$13.61 課金なし
メッセージ \$108.86(送受信 8,640 万件) \$0.17(受信のみ、20 件で 1 リクエスト換算)
実行時間 課金なし \$0(約 32 万 GB 秒、込みの 40 万以内)
合計 \$122.47(バックエンド別) \$0.17(月額 \$5 別)

ほかのサービスとの連携

Lambda Web Functions は IAM ロールで DynamoDB や S3 にアクセスキーなしで接続でき、ネットワークコネクターで VPC 内の RDS などにも届きます。ただし受け付けるのは HTTP だけで、独自ドメインや AWS WAF には CloudFront が必要です。Workers は KV、D1、R2、Queues などにバインディングで接続でき、Cron Triggers もありますが、AWS のサービスにはアクセスキーが必要で、VPC 内へは Workers VPC(ベータ)や Hyperdrive を経由します。

Event Functions と比べる

従来の Event Functions でも、各フレームワークのアダプターや AWS Lambda Web Adapter(LWA)で HTTP サーバーを動かせます。今回のアプリも、LWA のレイヤーと環境変数 3 つを追加するだけで index.js を変えずに動きました。

arm64・nodejs24.x・Function URL(RESPONSE_STREAM)、LWA の arm64 レイヤー(バージョン 30)で、同じ Mac から計測しました。

Lambda Web Functions Event Functions(LWA)
1 環境の同時処理数 デフォルト 64 1
CPU とメモリ 2 vCPU / 2 GB 固定 128〜10,240 MB
ランタイム Node.js 24 のみ Node.js、Python、Java など
課金 CPU 時間 + メモリ × 存続時間 実行時間(I/O 待ち含む)× 割り当てメモリ
無料枠 記載なし 月 100 万リクエスト + 40 万 GB 秒
呼び出し元 HTTP のみ HTTP、SQS、S3、EventBridge など
デプロイから最初の 200 まで 中央値 23.96 秒 中央値 6.57 秒(IAM ロール作成済み、ZIP 作成除く)
コールドスタート時の応答 中央値 690 ms 中央値 921 ms(1,024 MB)、793 ms(2,048 MB)
ウォーム時の応答 43〜57 ms 中央値 57〜59 ms
1 秒ごとに 45 件送る SSE 30 件目で切断(デフォルト) 全件受信(タイムアウト 60 秒)

料金は前節の条件で、Event Functions を 1,024 MB とし、I/O 待ちも課金されるため A は 1 リクエスト 200 ms(AWS の試算どおり)、B は 5 ms としました。無料枠は含めていません。

A: 月 1,000 万リクエスト B: 月 100 万リクエスト
Lambda Web Functions \$22.33 \$2.45
Event Functions(1,024 MB) \$28.67 \$0.27
Cloudflare Workers(有料プラン) \$14.40 \$5.00

I/O 待ちの長い A では Lambda Web Functions が Event Functions より安く、リクエストの少ない B では Event Functions が約 9 分の 1 で、無料枠にも収まります。なお LWA の README は、新しい Node.js の Web アプリには Web Functions を勧めています。

まとめ

  • 公開 URL まで約 24 秒、更新は 8 秒弱で、更新中の数秒間は新旧が混在する
  • コールドスタートは 0.5〜0.7 秒の上乗せ。最後のリクエストから約 6 分で実行環境が停止する
  • 同時アクセスで実行環境が増え、アカウントの vCPU 上限(検証環境では 200)に達すると同じアカウントのほかの関数まで 429 になる
  • SSE はバッファリングなしで届くが、デフォルト 30 秒のタイムアウトでエラーなしに打ち切られる。WebSocket は使えない
  • 長時間接続なら Workers と Durable Objects が大幅に安い(\$122.47 に対して \$0.17)。リクエストが多い場合も Workers が安い(\$14.40 対 \$22.33)
  • リクエストが少ない場合は Workers 有料プランより安い(\$2.45 対 \$5.00)が、Event Functions がさらに安い(\$0.27)。Event Functions でも多くのフレームワークがそのまま動くので、どちらを使うかは慎重に選ぶ必要がある

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?