I Tried Out Bedrock Streaming Responses with API Gateway

I Tried Out Bedrock Streaming Responses with API Gateway

The November 2025 update lets you break through API Gateway's 29-second limit. We tested response streaming with Bedrock and confirmed generative AI answers can be handled without the 10MB or timeout restrictions.
2025.11.22

This page has been translated by machine translation. View original

On November 19, 2025, Amazon API Gateway received an update that adds support for streaming responses.

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

Previously, API Gateway had limits such as a 29-second integration timeout and a 10MB payload size cap. With this update, generated data can be streamed to the client immediately in chunks, making it possible to work around these limits.

This time, I had the chance to build a Lambda function that uses streaming responses from Bedrock (Claude Haiku 4.5), together with a streaming-enabled API Gateway, using CloudFormation, and to try out how streaming responses behave. Here is what I found.

Test Environment

For this test, I deployed the following Lambda function and API Gateway with CloudFormation.

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

Lambda Function

  • As the wrapper, I used awslambda.streamifyResponse instead of exports.handler = async....
  • I output the chunked data with responseStream.write.
// ... (前略) ...
exports.handler = awslambda.streamifyResponse(async (event, responseStream, context) => {
  const httpResponseMetadata = { /* ... */ };

  // Required: send metadata
  responseStream = awslambda.HttpResponseStream.from(responseStream, httpResponseMetadata);

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

  for await (const chunk of response.body) {
    // Important: write the chunked data to the stream sequentially
    responseStream.write(text.delta.text);
  }

  responseStream.end();
});

API Gateway

  • I specified ResponseTransferMode: STREAM.
  • I appended /response-streaming-invocations to the end of the Uri.
  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 template used for the test
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リージョンについて教えてください"}'

Verifying the Behavior

Command Executed

I sent a request to the deployed API with the curl command.
To observe the streaming behavior, I added the --no-buffer option.

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) 今後の展望。各項目について具体例を交えて説明してください。"}'

Results

Via streaming, I was able to receive about 52KB of text over roughly a minute and a half.

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

I confirmed that processing which would have hit the 29-second timeout limit with the previous API Gateway was not disconnected even after 95 seconds thanks to streaming, and the response was received all the way to the end.

Summary

Until now, when implementing long-running generative AI responses or large data downloads, there were cases where you had no choice but to pick a configuration such as ALB (Application Load Balancer) + Fargate in order to work around API Gateway's limits.

With this update, these requirements can now be met while keeping a serverless configuration (API Gateway + Lambda). Please consider simplifying your architecture by moving to Lambda.


AWS re:Invent 2025まとめ情報

クラスメソッドは2025/12/1〜5にラスベガスで開催されたAWSカンファレンスイベント「re:Invent」を今年も特集しました。ぜひ一度ご覧ください。 re:Invent 2025ポータルサイトを見るAWS re:Inventはこちら

Share this article

AWSのお困り事はクラスメソッドへ