I Tried Out Bedrock Streaming Responses with API Gateway
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.
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.streamifyResponseinstead ofexports.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-invocationsto 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.
