API GatewayでBedrockのストリーム応答を試してみた
2025年11月19日、Amazon API Gatewayがストリーム応答をサポートするアップデートがありました。
従来の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化によるアーキテクチャの簡素化を検討してみてください。








