Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apigw-streaming-lambda-bedrock-cdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
cdk.out
*.js
!src/**/*.js
*.d.ts
package-lock.json
77 changes: 77 additions & 0 deletions apigw-streaming-lambda-bedrock-cdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# API Gateway REST Streaming with Lambda and Amazon Bedrock

This pattern deploys an API Gateway REST API with response streaming enabled, backed by a streaming Lambda function that returns real-time LLM responses from Amazon Bedrock using Server-Sent Events (SSE).

Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/apigw-streaming-lambda-bedrock-cdk

Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example.

## Requirements

* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in.
* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured
* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [Node.js 18+](https://nodejs.org/en/download/) installed
* [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting-started.html) installed
* [Amazon Bedrock model access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) enabled for Anthropic Claude Sonnet in your target region

## Architecture

```
┌──────────┐ ┌──────────────────┐ ┌──────────────────┐ ┌─────────────┐
│ Client │────▶│ API Gateway │────▶│ Lambda │────▶│ Bedrock │
│ (curl) │◀────│ REST (Stream) │◀────│ (Streaming) │◀────│ (Claude) │
└──────────┘ SSE └──────────────────┘ └──────────────────┘ └─────────────┘
```

## How it works

1. Client sends a POST request with a prompt to the API Gateway REST endpoint.
2. API Gateway forwards the request to a Lambda function using **streaming invocation** (`InvokeWithResponseStreaming`).
3. The Lambda function calls Amazon Bedrock's `InvokeModelWithResponseStream` API.
4. As Bedrock generates tokens, Lambda writes each chunk to the response stream.
5. API Gateway streams the response back to the client in real-time using chunked transfer encoding.
6. The client receives tokens as they are generated — time-to-first-byte is typically under 1 second.

## Deployment Instructions

1. Clone the repository:
```bash
git clone https://github.com/aws-samples/serverless-patterns
cd serverless-patterns/apigw-streaming-lambda-bedrock-cdk
```

2. Install dependencies:
```bash
npm install
```

3. Deploy the stack:
```bash
cdk deploy
```

4. Note the API endpoint URL from the stack outputs.

## Testing

1. Stream a response using curl:
```bash
curl --no-buffer -X POST \
'<ApiEndpoint>/prod/chat' \
-H 'Content-Type: application/json' \
-d '{"prompt": "Explain serverless computing in 3 paragraphs"}'
```

2. You should see the response stream in real-time, token by token.

## Cleanup

```bash
cdk destroy
```

----
Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.

SPDX-License-Identifier: MIT-0
7 changes: 7 additions & 0 deletions apigw-streaming-lambda-bedrock-cdk/bin/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { ApigwStreamingLambdaBedrockStack } from "../lib/apigw-streaming-lambda-bedrock-stack";

const app = new cdk.App();
new ApigwStreamingLambdaBedrockStack(app, "ApigwStreamingLambdaBedrockStack");
11 changes: 11 additions & 0 deletions apigw-streaming-lambda-bedrock-cdk/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"app": "npx ts-node --prefer-ts-exts bin/app.ts",
"watch": {
"include": ["**"],
"exclude": ["README.md", "cdk*.json", "**/*.d.ts", "**/*.js", "node_modules", "src"]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true
}
}
61 changes: 61 additions & 0 deletions apigw-streaming-lambda-bedrock-cdk/example-pattern.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"title": "API Gateway REST Streaming with Lambda and Amazon Bedrock",
"description": "Stream LLM responses from Amazon Bedrock through API Gateway REST API using Lambda response streaming for real-time AI chat experiences.",
"language": "TypeScript",
"level": "300",
"framework": "AWS CDK",
"introBox": {
"headline": "How it works",
"text": [
"This pattern deploys an API Gateway REST API with response streaming enabled, backed by a Lambda function that streams responses from Amazon Bedrock (Claude) in real-time using Server-Sent Events (SSE).",
"When a client sends a prompt, API Gateway forwards it to a streaming Lambda function. The Lambda invokes Bedrock with streaming enabled and progressively writes each text chunk to the response stream. The client receives tokens as they are generated, dramatically reducing time-to-first-byte.",
"This pattern is ideal for building AI chatbots, copilots, and interactive assistants where real-time streaming improves user experience."
]
},
"gitHub": {
"template": {
"repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-streaming-lambda-bedrock-cdk",
"templateURL": "serverless-patterns/apigw-streaming-lambda-bedrock-cdk",
"projectFolder": "apigw-streaming-lambda-bedrock-cdk",
"templateFile": "lib/apigw-streaming-lambda-bedrock-stack.ts"
}
},
"resources": {
"bullets": [
{
"text": "Building responsive APIs with Amazon API Gateway response streaming",
"link": "https://aws.amazon.com/blogs/compute/building-responsive-apis-with-amazon-api-gateway-response-streaming/"
},
{
"text": "Serverless strategies for streaming LLM responses",
"link": "https://aws.amazon.com/blogs/compute/serverless-strategies-for-streaming-llm-responses/"
},
{
"text": "Amazon Bedrock",
"link": "https://aws.amazon.com/bedrock/"
}
]
},
"deploy": {
"text": [
"cdk deploy"
]
},
"testing": {
"text": [
"See the GitHub repo for detailed testing instructions."
]
},
"cleanup": {
"text": [
"Delete the stack: <code>cdk destroy</code>."
]
},
"authors": [
{
"name": "Nithin Chandran R",
"bio": "Technical Account Manager at AWS",
"linkedin": "nithin-chandran-r"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import * as cdk from "aws-cdk-lib";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as apigateway from "aws-cdk-lib/aws-apigateway";
import * as iam from "aws-cdk-lib/aws-iam";
import * as logs from "aws-cdk-lib/aws-logs";
import { Construct } from "constructs";

export class ApigwStreamingLambdaBedrockStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);

const modelId = new cdk.CfnParameter(this, "BedrockModelId", {
type: "String",
default: "us.anthropic.claude-sonnet-4-20250514-v1:0",
description: "Bedrock model ID (inference profile) to use",
});

// Streaming Lambda function
const fn = new lambda.Function(this, "StreamingBedrockFn", {
runtime: lambda.Runtime.NODEJS_20_X,
handler: "index.handler",
code: lambda.Code.fromAsset("src"),
timeout: cdk.Duration.minutes(5),
memorySize: 256,
environment: {
MODEL_ID: modelId.valueAsString,
},
logRetention: logs.RetentionDays.ONE_WEEK,
});

fn.addToRolePolicy(
new iam.PolicyStatement({
actions: ["bedrock:InvokeModelWithResponseStream"],
resources: [
`arn:aws:bedrock:${this.region}:${this.account}:inference-profile/${modelId.valueAsString}`,
"arn:aws:bedrock:*::foundation-model/*",
],
})
);

// REST API with streaming integration
const api = new apigateway.RestApi(this, "StreamingApi", {
restApiName: "Bedrock Streaming API",
description: "API Gateway REST API with response streaming to Bedrock",
deployOptions: { stageName: "prod" },
});

const chatResource = api.root.addResource("chat");

// Add POST method with standard Lambda proxy integration
const method = chatResource.addMethod(
"POST",
new apigateway.LambdaIntegration(fn, { timeout: cdk.Duration.minutes(5) })
);

// Override integration URI to use response-streaming-invocations path
const cfnMethod = method.node.defaultChild as apigateway.CfnMethod;
cfnMethod.addPropertyOverride(
"Integration.Uri",
`arn:aws:apigateway:${this.region}:lambda:path/2021-11-15/functions/${fn.functionArn}/response-streaming-invocations`
);
cfnMethod.addPropertyOverride(
"Integration.ResponseTransferMode",
"STREAM"
);
cfnMethod.addPropertyOverride("Integration.TimeoutInMillis", 300000);

new cdk.CfnOutput(this, "ApiEndpoint", {
value: api.urlForPath("/chat"),
description: "POST your prompt to this URL to stream Bedrock responses",
});
new cdk.CfnOutput(this, "FunctionName", { value: fn.functionName });
}
}
20 changes: 20 additions & 0 deletions apigw-streaming-lambda-bedrock-cdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "apigw-streaming-lambda-bedrock-cdk",
"version": "1.0.0",
"bin": {
"app": "bin/app.ts"
},
"scripts": {
"build": "tsc",
"cdk": "cdk"
},
"dependencies": {
"aws-cdk-lib": "2.180.0",
"constructs": "10.4.2"
},
"devDependencies": {
"@types/node": "^22.0.0",
"ts-node": "^10.9.0",
"typescript": "~5.7.0"
}
}
65 changes: 65 additions & 0 deletions apigw-streaming-lambda-bedrock-cdk/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const {
BedrockRuntimeClient,
InvokeModelWithResponseStreamCommand,
} = require("@aws-sdk/client-bedrock-runtime");

const client = new BedrockRuntimeClient();
const MODEL_ID = process.env.MODEL_ID;

exports.handler = awslambda.streamifyResponse(
async (event, responseStream, _context) => {
const body = JSON.parse(event.body || "{}");
const prompt = body.prompt || "Hello";

const httpMetadata = {
statusCode: 200,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
};

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

try {
const command = new InvokeModelWithResponseStreamCommand({
modelId: MODEL_ID,
contentType: "application/json",
accept: "application/json",
body: JSON.stringify({
anthropic_version: "bedrock-2023-05-31",
max_tokens: 2048,
messages: [{ role: "user", content: prompt }],
}),
});

const response = await client.send(command);

for await (const event of response.body) {
if (event.chunk) {
const parsed = JSON.parse(
new TextDecoder().decode(event.chunk.bytes)
);
if (
parsed.type === "content_block_delta" &&
parsed.delta?.text
) {
responseStream.write(`data: ${JSON.stringify({ text: parsed.delta.text })}\n\n`);
}
}
}

responseStream.write("data: [DONE]\n\n");
} catch (err) {
responseStream.write(
`data: ${JSON.stringify({ error: err.message })}\n\n`
);
}

responseStream.end();
}
);
20 changes: 20 additions & 0 deletions apigw-streaming-lambda-bedrock-cdk/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["es2020"],
"declaration": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitThis": true,
"alwaysStrict": true,
"outDir": "build",
"rootDir": ".",
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"esModuleInterop": true
},
"exclude": ["node_modules", "build", "src"]
}