API GatewayでBedrockのストリーム応答を試してみた

API GatewayでBedrockのストリーム応答を試してみた

2025年11月のアップデートでAPI Gatewayの「29秒の壁」を突破することが可能になりました。レスポンスストリーミングをBedrockで試し、10MB制限やタイムアウトの制約を受けず、生成AIの回答を扱えることを確認しました。
2025.11.22

2025年11月19日、Amazon API Gatewayがストリーム応答をサポートするアップデートがありました。

https://aws.amazon.com/jp/blogs/compute/building-responsive-apis-with-amazon-api-gateway-response-streaming/

従来のAPI Gatewayには、29秒の統合タイムアウトや10MBのペイロードサイズ上限といった制限が存在しました。今回のアップデートにより、生成されたデータをチャンク(分割)単位で即座にクライアントへストリーミング送信できるようになり、これらの制限を回避した利用が可能になりました。

今回、Bedrock (Claude Haiku 4.5) のストリーム応答を利用するLambda関数と、ストリームをサポートした API Gateway を CloudFormationで構築。ストリーム応答の動作を試す機会がありましたので紹介します。

検証環境

今回の検証では、以下のLambda関数とAPI GatewayをCloudFormationでデプロイしました。

  • Runtime: Node.js 20.x
  • Model: Claude Haiku 4.5 (Bedrock)
  • Feature: API Gateway Response Streaming

Lambda関数

  • ラッパーとして exports.handler = async... ではなく、awslambda.streamifyResponse を使用しました。
  • 分割データを responseStream.write で出力しました
// ... (前略) ...
exports.handler = awslambda.streamifyResponse(async (event, responseStream, context) => {
  const httpResponseMetadata = { /* ... */ };

  // 必須: メタデータの送信
  responseStream = awslambda.HttpResponseStream.from(responseStream, httpResponseMetadata);

  // ... (Bedrock呼び出し処理) ...

  for await (const chunk of response.body) {
    // 重要: 分割データを順次ストリームに書き込む
    responseStream.write(text.delta.text);
  }

  responseStream.end();
});

API Gateway

  • ResponseTransferMode: STREAM を指定しました。
  • Uri の末尾に /response-streaming-invocations を付与しました
  StreamMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref RestApi
      ResourceId: !Ref StreamResource
      HttpMethod: POST
      AuthorizationType: NONE
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: POST
        Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2021-11-15/functions/${StreamingLambda.Arn}/response-streaming-invocations'
        ResponseTransferMode: STREAM
        TimeoutInMillis: 300000

CloudFormation

検証に使用したCloudFormationテンプレート
AWSTemplateFormatVersion: '2010-09-09'
Description: 'API Gateway Response Streaming with Lambda and Bedrock Haiku 4.5'

Resources:
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: BedrockInvokePolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - bedrock:InvokeModelWithResponseStream
                Resource: '*'

  StreamingLambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: BedrockStreamingFunction
      Runtime: nodejs20.x
      Handler: index.handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Timeout: 300
      Code:
        ZipFile: |
          const { BedrockRuntimeClient, InvokeModelWithResponseStreamCommand } = require("@aws-sdk/client-bedrock-runtime");

          const client = new BedrockRuntimeClient({ region: process.env.AWS_REGION });

          exports.handler = awslambda.streamifyResponse(async (event, responseStream, context) => {
            const httpResponseMetadata = {
              statusCode: 200,
              headers: {
                'Content-Type': 'text/plain',
                'Access-Control-Allow-Origin': '*'
              }
            };

            responseStream = awslambda.HttpResponseStream.from(responseStream, httpResponseMetadata);

            try {
              const body = JSON.parse(event.body || '{}');
              const prompt = body.prompt || "こんにちは";

              const command = new InvokeModelWithResponseStreamCommand({
                modelId: "jp.anthropic.claude-haiku-4-5-20251001-v1:0",
                contentType: "application/json",
                accept: "application/json",
                body: JSON.stringify({
                  anthropic_version: "bedrock-2023-05-31",
                  max_tokens: 100000,
                  messages: [{
                    role: "user",
                    content: prompt
                  }]
                })
              });

              const response = await client.send(command);

              for await (const chunk of response.body) {
                if (chunk.chunk?.bytes) {
                  const text = JSON.parse(new TextDecoder().decode(chunk.chunk.bytes));
                  if (text.type === 'content_block_delta' && text.delta?.text) {
                    responseStream.write(text.delta.text);
                  }
                }
              }

              responseStream.end();
            } catch (error) {
              responseStream.write(`Error: ${error.message}`);
              responseStream.end();
            }
          });

  LambdaInvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref StreamingLambda
      Action: lambda:InvokeFunction
      Principal: apigateway.amazonaws.com
      SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*'

  RestApi:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: BedrockStreamingAPI
      Description: API Gateway with response streaming for Bedrock

  StreamResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref RestApi
      ParentId: !GetAtt RestApi.RootResourceId
      PathPart: stream

  StreamMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref RestApi
      ResourceId: !Ref StreamResource
      HttpMethod: POST
      AuthorizationType: NONE
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: POST
        Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2021-11-15/functions/${StreamingLambda.Arn}/response-streaming-invocations'
        ResponseTransferMode: STREAM
        TimeoutInMillis: 300000

  OptionsMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref RestApi
      ResourceId: !Ref StreamResource
      HttpMethod: OPTIONS
      AuthorizationType: NONE
      Integration:
        Type: MOCK
        IntegrationResponses:
          - StatusCode: 200
            ResponseParameters:
              method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key'"
              method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'"
              method.response.header.Access-Control-Allow-Origin: "'*'"
            ResponseTemplates:
              application/json: ''
        RequestTemplates:
          application/json: '{"statusCode": 200}'
      MethodResponses:
        - StatusCode: 200
          ResponseParameters:
            method.response.header.Access-Control-Allow-Headers: true
            method.response.header.Access-Control-Allow-Methods: true
            method.response.header.Access-Control-Allow-Origin: true

  Deployment:
    Type: AWS::ApiGateway::Deployment
    DependsOn:
      - StreamMethod
      - OptionsMethod
    Properties:
      RestApiId: !Ref RestApi

  Stage:
    Type: AWS::ApiGateway::Stage
    Properties:
      RestApiId: !Ref RestApi
      DeploymentId: !Ref Deployment
      StageName: prod

Outputs:
  ApiEndpoint:
    Description: API Gateway endpoint URL
    Value: !Sub 'https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/prod/stream'

  TestCommand:
    Description: Test command using curl
    Value: !Sub |
      curl --no-buffer -X POST https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/prod/stream \
        -H "Content-Type: application/json" \
        -d '{"prompt":"日本のAWSリージョンについて教えてください"}'

動作確認

実行コマンド

デプロイしたAPIに対し、curl コマンドでリクエストを送信しました。
ストリーミングの挙動を確認するため、--no-buffer オプションを付与しています。

curl --no-buffer -X POST https://****.ap-northeast-1.amazonaws.com/prod/stream \
  -H "Content-Type: application/json" \
  -d '{"prompt":"日本のAWSリージョンについて、以下の観点から詳しく説明してください:1) 各リージョンの歴史と開設時期、2) 提供されているサービスの違い、3) アベイラビリティゾーンの構成、4) レイテンシーとパフォーマンス特性、5) 料金体系の違い、6) ディザスタリカバリー戦略での活用方法、7) コンプライアンスと規制対応、8) 今後の展望。各項目について具体例を交えて説明してください。"}'

実行結果

ストリーミングで約1分半かけて、約52KBのテキストを受信できました。

100 55068    0 54536  100   532    571      5  0:01:46  0:01:35  0:00:11   472
このような選択により、ビジネス要件に応じた最適なAWS利用戦略が実現できます。
 - Completed in 95.500s

これまでのAPI Gatewayであれば29秒でタイムアウト制限に抵触していた処理が、ストリーミングによって95秒かかっても切断されず、最後までレスポンスを受け取れていることが確認できました

まとめ

これまで、長時間実行される生成AIのレスポンスや大容量データのダウンロードを実装する場合、API Gatewayの制限を回避するためにALB (Application Load Balancer) + Fargate などの構成を選択せざるを得ないケースがありました。

今回のアップデートにより、サーバーレス構成(API Gateway + Lambda)のまま、これらの要件に対応できるようになりました。ぜひ、Lambda化によるアーキテクチャの簡素化を検討してみてください。

この記事をシェアする

FacebookHatena blogX

関連記事