はじめに
こんばんは、mirukyです。
今日は比較的ニッチなことをやります。
数秒かかるAPIでは、最後まで何も返らない場合と、完成した部分から返る場合で待ち時間の見え方が変わりますよね。実はAmazon API GatewayのREST APIでは、バックエンドから届いたレスポンスを少しずつクライアントへ転送できるんです。
今回は東京リージョンでLambdaとREST APIを作り、4つの文字列が1秒おきに届くか確かめます。AWSリソースの作成から後片付けまで、AWSマネジメントコンソールだけで進めました。
目次
1. レスポンスストリーミングの仕組み
通常のBUFFEREDは、API Gatewayがバックエンドのレスポンス全体を受け取ってからクライアントへ返します。STREAMは、受け取ったチャンクを順次転送します。
| 転送モード | クライアントへ返し始める時点 | 向いている処理 |
|---|---|---|
BUFFERED |
レスポンス全体が完成した後 | 小さく短い通常のAPI |
STREAM |
最初のチャンクを受け取った後 | 生成中の文章、進捗、大きな応答 |
利用できるのはREST APIのAWS_PROXYまたはHTTP_PROXY統合です。Lambdaプロキシ統合では、API GatewayがLambdaのInvokeWithResponseStreamを使います。HTTP APIにはこの設定がありません。
API Gatewayのレスポンスストリーミングは2025年11月に全AWSリージョンで提供され、Lambdaのレスポンスストリーミングも2026年4月に全商用リージョンへ拡大されました。今回の構成は次のとおりです。
| 項目 | 今回の設定 |
|---|---|
| リージョン | アジアパシフィック(東京)ap-northeast-1
|
| API | Regional REST API |
| メソッド | GET /stream |
| 統合 | Lambdaプロキシ統合、STREAM
|
| 認証 |
NONE。検証中だけ使い、確認後すぐ削除 |
| Lambda | Node.js 24.x、タイムアウト15秒 |
1つの応答は最大15分です。アイドルタイムアウトはRegionalとPrivateが5分、Edge-optimizedが30秒となります。STREAMでは、API Gatewayのエンドポイントキャッシュ、コンテンツ圧縮、レスポンスのVTL変換を使えません。
2. Lambda関数を作る
Lambda、API Gateway、IAM、CloudWatch Logsの作成・更新・削除権限と、Lambda実行ロールを渡すiam:PassRoleが必要です。今回の検証は数回のリクエスト、128MBのLambdaを1分未満、数KBのログに収まりました。無料利用枠を考慮しない保守的な試算でも0.001 USD未満ですが、利用中の設定や料金改定によって変わります。
まず、作成方法と関数名を決めるため、関数作成フォームの上部を確かめます。
作成方法は「一から作成」で、関数名には「qiita-stream-20260811-r2」が入力されています。
続けて、ランタイムと最初の実行権限を確かめます。
ランタイムは「Node.js 24.x」で、「アクセス許可」にはCloudWatch Logsへログをアップロードする権限を持つ実行ロールの説明が表示されています。
ストリームが閉じるまで処理できるよう、一般設定の編集内容を確かめます。
タイムアウト欄は0分15秒で、専用の実行ロールが選択されています。
保存が反映されたか、一般設定の要約を見ておきます。
保存後の「一般設定」には、タイムアウトが0分15秒と表示されています。
index.mjsは次の内容に置き換えます。
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
const wait = (milliseconds) =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
export const handler = awslambda.streamifyResponse(
async (_event, responseStream) => {
const metadata = {
statusCode: 200,
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "no-cache",
"Access-Control-Allow-Origin": "*",
},
};
responseStream = awslambda.HttpResponseStream.from(
responseStream,
metadata,
);
// 1秒ごとに4つのチャンクを返す
const chunks = Readable.from(
(async function* () {
for (let number = 1; number <= 4; number += 1) {
yield `chunk=${number}\n`;
if (number < 4) {
await wait(1000);
}
}
})(),
);
await pipeline(chunks, responseStream);
},
);
streamifyResponse()でストリーミング対応のハンドラーにし、HttpResponseStream.from()でHTTPステータスとヘッダーを先に渡します。pipeline()は4つのチャンクを最後まで書き込み、ストリームを閉じる役割を持ちます。今回は別オリジンの検証用HTMLから呼び出すため、CORSヘッダーも返します。
更新後のコードの冒頭とハンドラーを確かめます。
更新後のコードソースには、Readableとpipelineのimport、およびstreamifyResponseを使うハンドラーが表示されています。
APIへつなぐ前に、関数が最後まで動くか同期呼び出しで確かめます。
テストイベントは同期呼び出しで、イベント名は「streamTestR2」です。
このテストは関数の成功確認に使います。
Lambdaコンソールの実行結果は成功で、Response欄にはchunk=1からchunk=4までがまとまって表示されています。Lambdaの公式ドキュメントのとおり、コンソールからの呼び出し結果はバッファーされるため、これだけでは途中受信を判定できません。
3. REST APIを作る
API GatewayでRegional REST APIを作り、/streamリソースにGETメソッドを追加します。API名と統合先のLambda関数名はqiita-stream-20260811-r2です。
Lambdaから受け取ったチャンクをそのまま転送するため、完成した統合設定を確かめます。
統合リクエストの設定では、統合タイプが「Lambda」、Lambdaプロキシ統合が「True」、レスポンス転送モードが「ストリーム」です。
次に、クライアントから呼ぶためのステージを確かめます。
ステージ名は「demo」で、呼び出しURLとアクティブなデプロイが表示されています。今回のメソッド認証はNONEです。公開URLを知っていれば誰でも呼び出せるため、動作確認が終わるまでの一時設定として扱います。
4. 少しずつ届くか確かめる
ブラウザーで途中受信を見るため、次のHTMLをstreaming-client.htmlとして保存します。endpointは、ステージの呼び出しURLの末尾に/streamを付けた値へ置き換えてください。
<!doctype html>
<meta charset="utf-8">
<title>Streaming response check</title>
<pre id="log"></pre>
<script type="module">
// デプロイ後の呼び出しURLへ置き換える
const endpoint = "https://API_ID.execute-api.ap-northeast-1.amazonaws.com/demo/stream";
const log = document.querySelector("#log");
const started = performance.now();
const response = await fetch(endpoint);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffered = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffered += decoder.decode(value, { stream: true });
let newline;
while ((newline = buffered.indexOf("\n")) !== -1) {
const line = buffered.slice(0, newline);
buffered = buffered.slice(newline + 1);
const seconds = ((performance.now() - started) / 1000).toFixed(2);
log.textContent += `${line} +${seconds}s\n`;
}
}
</script>
保存したHTMLをブラウザーで開きます。まず、最初の1行を読んだ直後に一時停止したのがこんな感じです。
先頭だけを読む記録モードでは、chunk=1が+0.07秒で届き、その時点で読み取りを止めています。レスポンス完了前に先頭チャンクを受け取れたことが分かります。
API GatewayのTestInvokeMethodもストリームを一時的にバッファーします。途中受信の判定には、デプロイ後のURLとストリーム対応クライアントを使います。
続けて、最後まで読み取った記録を確かめます。
通常モードでは4チャンクが+0.09秒、+1.09秒、+2.10秒、+3.10秒に届き、全体は3.10秒でした。約1秒の間隔で行が増えており、Lambdaが書き込んだ順番と一致します。
5. 実際のAPIへ入れる前に確認すること
ストリーミングへ変えると、レスポンスが完成する前にHTTPステータスとヘッダーが確定します。本文の途中で失敗しても、送信済みの200を後から500へ変更できません。SSEならエラーイベント、改行区切りJSONならエラー用レコードを決めるなど、本文内の失敗表現が必要です。
API Gatewayは最初の10MBを帯域制限なしで転送し、それを超えた部分を2MB/秒に制限します。API Gateway側にストリーミング専用の上乗せ料金はありませんが、10MBを超えるレスポンスは10MB単位で複数リクエストとして数えられます。Lambda側は応答ストリームへ書き込んだデータ量が課金対象で、1リクエストの最初の6MBは無料です。月間の無料枠やGBあたりの料金は利用リージョンの料金ページで確認します。
おわりに
ここまでお付き合いいただきありがとうございます。
ニッチといえば、ニッチですが、個人的に実用的だとも思います。
ではまた、お会いしましょう。
参考リンク
AWS公式ドキュメント
- Stream the integration response for your proxy integrations in API Gateway - AWS
- Configure a Lambda proxy integration with payload response streaming in API Gateway - AWS
- Set up a Lambda proxy integration with payload response streaming in API Gateway - AWS
- Writing response streaming-enabled Lambda functions - AWS
- Configuring a Lambda function to stream responses - AWS
- Deploy REST APIs in API Gateway - AWS
- Troubleshoot issues with response streaming in API Gateway - AWS
- Viewing CloudWatch logs for Lambda functions - AWS
AWS公式発表
- Amazon API Gateway now supports response streaming for REST APIs - AWS
- AWS Lambda response streaming is now available in all commercial AWS Regions - AWS










