Serverless Operations, inc

>_cd /blog/id_s5vcc0hdcfix

title

AWS Lambda Web Adaptor で HTTP ストリーミングレスポンスを使う

前回の記事、AWS Lambda Web AdaptorがGAしたので触ってみるに引き続き、AWS Lambda Web Adaptor を操作していきます。今日は HTTP ストリーミングレスポンスです。

HTTP ストリーミングレスポンス とは?

Webサーバーがクライアントにデータを返す方法には2種類あります。処理が完全に終わってからレスポンス全体を一括で返す従来の方式と、データが生成されるたびに少しずつリアルタイムで送り続けるストリーミング方式です。

Node.jsでは res.json()res.send() が前者にあたり、res.write()res.end() の組み合わせが後者にあたります。特別な宣言やフラグは不要で、res.write() を使った時点で自動的にストリーミングの挙動になります。res.write() は呼び出されるたびに即座にデータをクライアントへ送信し、res.end() が呼ばれるまで接続を維持し続けます。

ストリーミングレスポンスがもっと目立って使われるケースは、AIのリアルタイム表示が挙げられます。ChatGPTのように文字が1文字ずつ流れるように表示される体験は、LLMがトークンを生成するたびにストリーミングで送ることで実現しています。全部生成し終わってから返す方式だと、ユーザーは数十秒間ただ待つだけになってしまいます。

大容量ファイルの転送でも有効です。動画や大きなドキュメントをメモリに丸ごと載せてから返すのではなく、読み込みながら順次送ることでメモリ消費を抑えられます。進捗のリアルタイム報告にも使えます。時間のかかるバッチ処理や変換処理の途中経過を、処理しながらクライアントに逐次通知するような用途です。

AWS サーバレス環境における対応状況

2023年4月、AWSはこの時期に2つの重要な対応を同時に行いました。まずLambdaがFunction URLを通じたレスポンスストリーミングに公式対応しました。ただしこの時点では、Lambda専用の awslambda.streamifyResponse() という書き方が必要で、既存のWebフレームワークのコードをそのまま使うことはできませんでした。

これを受けLambda Web Adapter v0.7.0がストリーミングに対応しました。これにより、Expressなどの通常のWebフレームワークのコード(res.write() など)をそのままLambdaのストリーミングとして動かせるようになりました。Lambda専用のAPIを意識する必要がなくなったという点で、開発者体験が大きく向上したリリースです。

この時点ではストリーミングにはFunction URLが必須で、API Gatewayを経由するとレスポンスがバッファリングされてしまうという制約がありました。

2025年11月、長らく制約だったAPI Gateway(REST API)がついてストリーミングに対応しました。統合設定のレスポンス転送モードを STREAM に指定するだけで有効になり、タイムアウトも最大15分まで設定できるようになりました。

Function URLと異なりAPI Gatewayにはオーソライザー・WAF・カスタムドメインなど豊富な機能があるため、本番のAIアプリケーションでもストリーミングを安心して使える環境が整いました。ただしHTTP APIは引き続き非対応で、ストリーミングが使えるのはREST APIのみです。

2026年3月、Lambda Web AdapterがGAとなりました。ストリーミング機能自体はv0.7.0から提供されていましたが、その後各フレームワーク向けのサンプルが拡充されながら、2026年3月のv1.0.0でGAとなった、という歴史的背景があります。

さっそくやってみる

まずは前回の手順を終わらせ置きます。そのあと app.jstemplate.yaml の2つを以下に入れ替えて再度デプロイを行います。

const express = require('express');
const app = express();

app.use(express.json());

app.get('/', (req, res) => {
  res.json({ message: 'Hello from Lambda Web Adapter!' });
});

app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

// ストリーミングエンドポイント
app.get('/stream', async (req, res) => {
  const message = 'Lambda Web Adapter でストリーミングを実現しています!';
  const words = message.split('');

  res.setHeader('Content-Type', 'text/plain; charset=utf-8');
  res.setHeader('Transfer-Encoding', 'chunked');
  res.setHeader('X-Accel-Buffering', 'no');

  for (const char of words) {
    res.write(char);
    // 少し待って1文字ずつ送る
    await new Promise(resolve => setTimeout(resolve, 100));
  }

  res.end();
});

const port = process.env.PORT || 8080;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Globals:
  Function:
    Timeout: 30

Resources:
  ExpressFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ./
      Handler: bootstrap
      Runtime: nodejs20.x
      MemorySize: 512
      Environment:
        Variables:
          AWS_LAMBDA_EXEC_WRAPPER: /opt/bootstrap
          AWS_LWA_PORT: "8080"
          AWS_LWA_INVOKE_MODE: response_stream
      Layers:
        - !Sub arn:aws:lambda:${AWS::Region}:753240598075:layer:LambdaAdapterLayerX86:27
      FunctionUrlConfig:
        AuthType: NONE
        InvokeMode: RESPONSE_STREAM
      Events:
        Api:
          Type: HttpApi
          Properties:
            Path: /{proxy+}
            Method: ANY
        Root:
          Type: HttpApi
          Properties:
            Path: /
            Method: ANY

Outputs:
  ApiUrl:
    Value: !Sub https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/
  FunctionUrl:
    Value: !GetAtt ExpressFunctionUrl.FunctionUrl

sam buildsam buildを実行すると新しく /stream というエンドポイントが出来上がります。

Outputs
-------------------------------------------------------------------------------------------------------------------------------------------------------------   
Key                 ApiUrl
Description         -
Value               https://2xxxxx74.execute-api.ap-northeast-1.amazonaws.com/

Key                 FunctionUrl
Description         -
Value               https://zxxxxxxxjpd7jnbct2b40zfuem.lambda-url.ap-northeast-1.on.aws/
-------------------------------------------------------------------------------------------------------------------------------------------------------------   
curl --no-buffer https://zxxxxxxxjpd7jnbct2b40zfuem.lambda-url.ap-northeast-1.on.aws/stream

FunctionUrl側でホスティングされているエンドポイントに /stream でアクセスを行うと、こんな感じで1文字づつ表示されていきます。

res.write でそのままストリーミングを実現できていることがポイントです。

  const message = 'Lambda Web Adapter でストリーミングを実現しています!';
  const words = message.split('');

  res.setHeader('Content-Type', 'text/plain; charset=utf-8');
  res.setHeader('Transfer-Encoding', 'chunked');
  res.setHeader('X-Accel-Buffering', 'no');

  for (const char of words) {
    res.write(char);
    // 少し待って1文字ずつ送る
    await new Promise(resolve => setTimeout(resolve, 100));
  }

  res.end();
});

Lambda 標準機能であれば以下の通り専用の書式が必要となってしまいます。

export const handler = awslambda.streamifyResponse(
  async (event, responseStream, context) => {
    responseStream.write('hello');
    await new Promise(r => setTimeout(r, 1000));
    responseStream.write(' world');
    responseStream.end();
  }
);

Share

Facebook->X->
Back
to list
<-