
I built an API with authentication, streaming, and session management using Strands Agents + Lambda + API Gateway
This page has been translated by machine translation. View original
Introduction
Hello, I'm Kamino from the Consulting Department, and I love supermarkets.
Are you all building AI agents with Strands Agents?
When it comes to deployment destinations for agents built with Strands Agents, AgentCore is the standard choice, but looking at the official documentation, Lambda is also listed as an option.
API Gateway also added response streaming support in November 2025. So I wondered if we could quickly build a streaming-capable agent API with Lambda + API Gateway, and decided to try it out!
The code for this article is published on GitHub.
The code from the article body is in the v1 branch of the repository. After cloning, you can deploy everything at once with pnpm install && pnpm deploy.
git clone -b v1 https://github.com/yuu551/lambda-api-strands.git
cd lambda-api-strands
pnpm install
pnpm deploy
Prerequisites
The versions confirmed to work at the time of writing are as follows. Please refer to the repository's lockfile for exact dependencies.
| Item | Version / Setting |
|---|---|
| Region | us-east-1 |
| Node.js | 24.16.0 |
| Python | 3.13 (AgentCore Memory version only) |
| Package management | pnpm 11.11.0 |
| aws-cdk-lib | 2.261.0 |
| @strands-agents/sdk | 1.10.0 |
| hono | 4.12.31 |
| zod | 4.4.3 |
| @aws-cdk/aws-lambda-python-alpha | 2.262.0-alpha.0 (AgentCore Memory version only) |
| Model | Claude Haiku 4.5 |
Architecture
Here is what we will ultimately build in this article, as a whole.

In the first half, we will only build ① the S3 SessionManager version. The configuration is simple, with only one Lambda, API Gateway, Cognito, and an S3 bucket as resources. In the second half we will add a second Lambda using AgentCore Memory, but let's start with this minimal configuration first.
The client authenticates with Cognito in advance to obtain an ID token, and calls the API with it in the Authorization header.
The API Gateway Cognito authorizer validates the token, so there is no need to write authentication logic on the Lambda side.
Why use Hono
If you just want to stream from Lambda, you could write it directly using awslambda.streamifyResponse.
Using Hono allows you to write streaming processing concisely and handle routing as well. It's also a framework I personally like, so I used Hono this time!
A similar implementation using Hono is also introduced in the following article on our company blog. Please refer to it as needed.
Implementation
This time I implemented it with the following structure! It turned out to be simpler than expected.
lambda-api-strands/
├── bin/
│ └── app.ts # CDK entry point
├── lib/
│ └── agent-api-stack.ts # Stack definition
├── lambda/
│ └── handler.ts # Hono + Strands Agents (agent body)
├── cdk.json
├── package.json
└── tsconfig.json
Project Setup
First, install dependencies with pnpm.
pnpm init
pnpm add aws-cdk-lib constructs @strands-agents/sdk @aws-sdk/client-s3 hono zod @aws-cdk/aws-lambda-python-alpha
pnpm add -D aws-cdk typescript tsx esbuild @types/node
Lambda Handler
Let's implement a handler combining Hono + Strands Agents + S3 SessionManager.
import { Hono } from 'hono'
import { streamHandle } from 'hono/aws-lambda'
import { streamText } from 'hono/streaming'
import { Agent, BedrockModel, SessionManager, tool } from '@strands-agents/sdk'
import { S3Storage } from '@strands-agents/sdk/storage'
import z from 'zod'
type Bindings = {
event: {
requestContext: {
authorizer: {
claims: { sub: string; email?: string }
}
}
}
}
const SYSTEM_PROMPT = `You are a helpful assistant that responds in Japanese.
For questions that require the current time, use the get_current_time tool to answer accurately.
Keep your answers concise.`
const getCurrentTime = tool({
name: 'get_current_time',
description: 'Returns the current time in Japan Standard Time (JST).',
inputSchema: z.object({}),
callback: () =>
new Date().toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' }),
})
const app = new Hono<{ Bindings: Bindings }>()
app.post('/invoke', async (c) => {
const { prompt, sessionId } = await c.req.json<{ prompt: string; sessionId?: string }>()
if (!prompt) {
return c.json({ error: 'prompt is required' }, 400)
}
// Get the user-specific sub from Cognito JWT claims
const userSub = c.env.event.requestContext.authorizer.claims.sub
const sessionManager = new SessionManager({
sessionId: sessionId ?? 'default',
// Isolate S3 prefix per user
storage: new S3Storage(process.env.SESSION_BUCKET!, {
prefix: `sessions/${userSub}/`,
}),
})
const agent = new Agent({
model: new BedrockModel({ modelId: process.env.MODEL_ID }),
systemPrompt: SYSTEM_PROMPT,
tools: [getCurrentTime],
sessionManager,
printer: false,
})
return streamText(c, async (stream) => {
for await (const chunk of agent.stream(prompt)) {
if (
chunk.type === 'modelStreamUpdateEvent' &&
chunk.event.type === 'modelContentBlockDeltaEvent' &&
chunk.event.delta.type === 'textDelta'
) {
await stream.write(chunk.event.delta.text)
}
}
})
})
export const handler = streamHandle(app)
The Cognito ID token contains a value called sub that uniquely identifies that user. After validating the token, the API Gateway authorizer decodes its contents and places them in the Lambda event. This means the Lambda side doesn't need to parse the token itself, and can obtain the logged-in user's information just by reading c.env.event.requestContext.authorizer.claims.sub.
By embedding this sub at the beginning of the S3 key, the storage path is split into sessions/{sub}/{sessionId}/..., so even if someone specifies the same sessionId as another user, a different sub means a different path, preventing conversation histories from mixing.
Streaming is handled inside Hono's streamText().
Events have a two-level structure: the outer modelStreamUpdateEvent contains modelContentBlockDeltaEvent, and those with delta.type of textDelta are text increments. Passing these to stream.write() returns them to the client sequentially.
Session management can be achieved by setting S3Storage in SessionManager. Conversations are identified by the sessionId in the request body, and the same sessionId will automatically restore the previous conversation history. No DynamoDB table design or custom session management code is needed. Easy to implement, which is great.
CDK Stack
Here is how I structured the main stack definition.
import * as cdk from 'aws-cdk-lib'
import * as apigateway from 'aws-cdk-lib/aws-apigateway'
import * as cognito from 'aws-cdk-lib/aws-cognito'
import * as iam from 'aws-cdk-lib/aws-iam'
import * as lambda from 'aws-cdk-lib/aws-lambda'
import { NodejsFunction, OutputFormat } from 'aws-cdk-lib/aws-lambda-nodejs'
import * as s3 from 'aws-cdk-lib/aws-s3'
import { Construct } from 'constructs'
const MODEL_ID = 'global.anthropic.claude-haiku-4-5-20251001-v1:0'
export class AgentApiStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props)
// Cognito User Pool
const userPool = new cognito.UserPool(this, 'AgentUserPool', {
selfSignUpEnabled: false,
signInAliases: { email: true },
removalPolicy: cdk.RemovalPolicy.DESTROY,
})
const userPoolClient = userPool.addClient('AgentApiClient', {
authFlows: { userPassword: true },
})
// S3 bucket for session storage
const sessionBucket = new s3.Bucket(this, 'SessionBucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
})
// Lambda (Hono + Strands Agents TypeScript SDK)
const agentFunction = new NodejsFunction(this, 'AgentFunction', {
entry: 'lambda/handler.ts',
handler: 'handler',
runtime: lambda.Runtime.NODEJS_24_X,
architecture: lambda.Architecture.ARM_64,
timeout: cdk.Duration.minutes(5),
memorySize: 512,
environment: {
MODEL_ID,
SESSION_BUCKET: sessionBucket.bucketName,
},
bundling: {
format: OutputFormat.ESM,
banner:
"import { createRequire } from 'module'; const require = createRequire(import.meta.url);",
},
})
sessionBucket.grantReadWrite(agentFunction)
agentFunction.addToRolePolicy(
new iam.PolicyStatement({
actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'],
resources: ['*'],
}),
)
// API Gateway (REST API) + Cognito authorizer + streaming
const api = new apigateway.RestApi(this, 'AgentApi', {
restApiName: 'strands-agent-api',
deployOptions: { stageName: 'v1' },
})
const authorizer = new apigateway.CognitoUserPoolsAuthorizer(this, 'AgentApiAuthorizer', {
cognitoUserPools: [userPool],
})
const methodOptions: apigateway.MethodOptions = {
authorizer,
authorizationType: apigateway.AuthorizationType.COGNITO,
}
// POST /invoke: respond with streaming
api.root.addResource('invoke').addMethod(
'POST',
new apigateway.LambdaIntegration(agentFunction, {
responseTransferMode: apigateway.ResponseTransferMode.STREAM,
timeout: cdk.Duration.minutes(5),
}),
methodOptions,
)
new cdk.CfnOutput(this, 'ApiEndpoint', { value: api.url })
new cdk.CfnOutput(this, 'UserPoolId', { value: userPool.userPoolId })
new cdk.CfnOutput(this, 'UserPoolClientId', { value: userPoolClient.userPoolClientId })
}
}
Since we're using NodejsFunction, it automatically bundles handler.ts with esbuild. The Strands SDK, Hono, and zod are all included in the bundle, so no Lambda layer is needed.
To enable streaming, set responseTransferMode to STREAM in LambdaIntegration.
The main configuration values are as follows.
| Configuration Item | Value | Description |
|---|---|---|
| runtime | Node.js 24 | Managed runtime with Lambda streaming support |
| architecture | ARM64 | |
| responseTransferMode | STREAM | Enables API Gateway response streaming |
| timeout | 5 minutes | Lambda function timeout. Set longer in preparation for long responses |
| bundling.format | ESM | Bundle format for NodejsFunction |
Deploy
Once implemented, let's deploy.
pnpm install
npx cdk deploy
✅ AgentApiStack
✨ Deployment time: 61.86s
Outputs:
AgentApiStack.ApiEndpoint = https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/v1/
AgentApiStack.UserPoolClientId = xxxxxxxxxxxxxxxxxxxxxxxxxx
AgentApiStack.UserPoolId = us-east-1_xxxxxxxxx
Deployment succeeded! Let's verify the operation.
Operation Verification
Let's actually try it out!
Creating a Test User
Create a test user in Cognito. Here we use the user pool ID output during deployment.
USER_POOL_ID=us-east-1_xxxxxxxxx
aws cognito-idp admin-create-user \
--user-pool-id $USER_POOL_ID \
--username test@example.com \
--message-action SUPPRESS
aws cognito-idp admin-set-user-password \
--user-pool-id $USER_POOL_ID \
--username test@example.com \
--password 'YourPassword123!' \
--permanent
Verifying Authentication
First, let's try calling without a token.
curl -X POST "$API_ENDPOINT/invoke" \
-H "Content-Type: application/json" \
-d '{"prompt": "Hello"}'
{"message":"Unauthorized"}
It's properly rejected with a 401!
Verifying Streaming
Obtain a token from Cognito and call with streaming.
CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxx
ID_TOKEN=$(aws cognito-idp initiate-auth \
--auth-flow USER_PASSWORD_AUTH \
--client-id $CLIENT_ID \
--auth-parameters USERNAME=test@example.com,PASSWORD='YourPassword123!' \
--query 'AuthenticationResult.IdToken' \
--output text)
curl -N -X POST "$API_ENDPOINT/invoke" \
-H "Content-Type: application/json" \
-H "Authorization: $ID_TOKEN" \
-d '{"prompt":"Name 3 of your favorite AWS services","sessionId":"test-1"}'
Characters were displayed sequentially in the terminal! When I measured the chunk arrival timing to verify whether it was truly streaming, the results were as follows.
[+ 1.51s] chunk 1: 'AWSの好きなサービスを3つ'
[+ 1.54s] chunk 2: '挙げる'
[+ 1.55s] chunk 3: 'とす'
(omitted)
[+ 2.95s] chunk 63: '的なサービ'
[+ 2.98s] chunk 66: 'スです。'
--- first: +1.51s, chunks: 74, done: +3.26s
The first chunk arrived approximately 1.5 seconds after the request, and 74 chunks arrived sequentially!
Verifying Multi-turn Conversation
Let's verify that S3 SessionManager is working. Let's call with the same sessionId twice.
curl -N -X POST "$API_ENDPOINT/invoke" \
-H "Content-Type: application/json" \
-H "Authorization: $ID_TOKEN" \
-d '{"prompt":"Hello, I am Kamino. My favorite food is curry. Please remember that.","sessionId":"blog-test-1"}'
Hello, Kamino! Nice to meet you.
So your favorite food is curry. Curry is delicious!
Thank you for introducing yourself. I will keep this information in mind for future conversations.
curl -N -X POST "$API_ENDPOINT/invoke" \
-H "Content-Type: application/json" \
-H "Authorization: $ID_TOKEN" \
-d '{"prompt":"Do you remember my name and favorite food?","sessionId":"blog-test-1"}'
Yes, I remember!
Your name: Kamino
Favorite food: Curry
That's right. I will make use of this information in future questions and conversations!
It's properly responding based on the previous conversation!
When I looked at the S3 contents, the paths were separated per user by the Cognito sub.
sessions/94085448-7071-7023-fd3e-d4866412213a/session/blog-test-1/scopes/agent/agent/snapshots/snapshot_latest.json
It has the structure sessions/{sub}/{sessionId}/...! Even if a different user calls with the same sessionId, the path changes, so conversation histories are separated per user at the application level.
Lambda is stateless, but thanks to S3 SessionManager, multi-turn conversation is achieved.
The S3 version is working end-to-end!!
Also Trying the AgentCore Memory Version
Using the AgentCore Memory SessionManager allows you to automatically extract user preferences and factual information from conversations, and easily integrate long-term memory that can be referenced across different sessions.
However, as of the time of writing, AgentCoreMemorySessionManager for Strands is provided for Python only. Therefore, I added a separate Python Lambda using FastAPI + Lambda Web Adapter, separate from the Hono (TypeScript) one. The language changes only here, but if you want to stream from Lambda while also using AgentCore Memory, the current approach is to start from Python.
Creating AgentCore Memory Resources
AgentCore Memory can be created from CDK, so let's add it to the stack. Three long-term memory strategies are enabled.
import * as agentcore from 'aws-cdk-lib/aws-bedrockagentcore'
// AgentCore Memory (align paths with RetrievalConfig on the Python side)
const memory = new agentcore.Memory(this, 'AgentMemory', {
memoryName: 'lambda_api_strands_memory',
expirationDuration: cdk.Duration.days(7),
memoryStrategies: [
agentcore.MemoryStrategy.usingUserPreference({
strategyName: 'UserPreference',
namespaces: ['/preferences/{actorId}/'],
}),
agentcore.MemoryStrategy.usingSemantic({
strategyName: 'SemanticFacts',
namespaces: ['/facts/{actorId}/'],
}),
agentcore.MemoryStrategy.usingSummarization({
strategyName: 'SessionSummary',
namespaces: ['/summaries/{actorId}/{sessionId}/'],
}),
],
})
const cfnMemory = memory.node.findChild('Memory') as agentcore.CfnMemory
cfnMemory.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY)
The {actorId} in the namespace is a placeholder that is automatically replaced at request time. Since we will pass the Cognito sub as actor_id on the Python side later, memories are separated per user.
Python Lambda Handler
Here is the handler for FastAPI + Strands Python SDK + AgentCore Memory SessionManager.
import json
import os
from datetime import datetime, timedelta, timezone
from bedrock_agentcore.memory.integrations.strands.config import (
AgentCoreMemoryConfig,
RetrievalConfig,
)
from bedrock_agentcore.memory.integrations.strands.session_manager import (
AgentCoreMemorySessionManager,
)
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse, StreamingResponse
from pydantic import BaseModel
from strands import Agent, tool
SYSTEM_PROMPT = """You are a helpful assistant that responds in Japanese.
For questions that require the current time, use the get_current_time tool to answer accurately.
Keep your answers concise.
"""
app = FastAPI()
@tool
def get_current_time() -> str:
"""Returns the current time in Japan Standard Time (JST)."""
jst = timezone(timedelta(hours=9))
return datetime.now(jst).strftime("%Y-%m-%d %H:%M:%S")
class InvokeRequest(BaseModel):
prompt: str
session_id: str | None = None
def _get_user_sub(request: Request) -> str | None:
"""Returns the Cognito sub. Returns None if not obtainable (caller handles 401)."""
# LWA (response streaming mode) passes requestContext in x-amzn-request-context
raw = request.headers.get("x-amzn-request-context")
if not raw:
return None
try:
ctx = json.loads(raw)
sub = ctx.get("authorizer", {}).get("claims", {}).get("sub")
return sub if isinstance(sub, str) and sub else None
except (json.JSONDecodeError, TypeError, AttributeError):
return None
@app.post("/invoke-memory")
async def invoke_memory(req: InvokeRequest, request: Request):
# actor_id is the user isolation boundary for Memory, so don't fall back to a shared ID
user_sub = _get_user_sub(request)
if user_sub is None:
return JSONResponse({"message": "Unauthorized"}, status_code=401)
session_id = req.session_id or "default"
config = AgentCoreMemoryConfig(
memory_id=os.environ["MEMORY_ID"],
session_id=session_id,
actor_id=user_sub,
retrieval_config={
"/preferences/{actorId}/": RetrievalConfig(top_k=5, relevance_score=0.5),
"/facts/{actorId}/": RetrievalConfig(top_k=10, relevance_score=0.3),
"/summaries/{actorId}/{sessionId}/": RetrievalConfig(
top_k=3, relevance_score=0.5
),
},
)
session_manager = AgentCoreMemorySessionManager(
config, region_name=os.environ.get("MEMORY_REGION", "us-east-1")
)
agent = Agent(
model=os.environ["MODEL_ID"],
system_prompt=SYSTEM_PROMPT,
tools=[get_current_time],
session_manager=session_manager,
callback_handler=None,
)
async def generate():
# Ensure flush even if an exception or client disconnection occurs midway
try:
async for event in agent.stream_async(req.prompt):
if "data" in event:
yield event["data"]
finally:
session_manager.close()
return StreamingResponse(generate(), media_type="text/plain; charset=utf-8")
The structure is similar to the Hono version, but there are some differences.
For streaming, use agent.stream_async(). This is an async generator equivalent to the TypeScript version's agent.stream(), and text increments come in the event's data key.
User isolation is achieved simply by passing the Cognito sub as actor_id in AgentCoreMemoryConfig. In the Hono version, we manually embedded the sub in the S3 prefix, but AgentCore Memory automatically replaces {actorId} in the namespace.
Note that the method for obtaining JWT claims differs from the Hono version.
In Lambda Web Adapter streaming mode, the API Gateway requestContext is passed as JSON in the x-amzn-request-context header. With Hono, the event could be retrieved via c.env.event, but with FastAPI via LWA, it is extracted from the request header.
Adding to CDK
Add a Python Lambda to the CDK stack. Using PythonFunction from @aws-cdk/aws-lambda-python-alpha will automatically bundle dependencies from requirements.txt via Docker. It feels the same as NodejsFunction on the TypeScript side.
Add @aws-cdk/aws-lambda-python-alpha to the imports.
import * as python from '@aws-cdk/aws-lambda-python-alpha'
CDK additions (append to agent-api-stack.ts)
// Python Lambda (FastAPI + Strands Python SDK + AgentCore Memory)
const lwaLayer = lambda.LayerVersion.fromLayerVersionArn(
this,
'LwaLayer',
`arn:aws:lambda:${this.region}:753240598075:layer:LambdaAdapterLayerArm64:25`,
)
const memoryFunction = new python.PythonFunction(this, 'MemoryFunction', {
entry: 'lambda/memory',
index: 'main.py',
runtime: lambda.Runtime.PYTHON_3_13,
architecture: lambda.Architecture.ARM_64,
timeout: cdk.Duration.minutes(5),
memorySize: 512,
layers: [lwaLayer],
environment: {
MODEL_ID,
MEMORY_ID: memory.memoryId,
MEMORY_REGION: this.region,
AWS_LAMBDA_EXEC_WRAPPER: '/opt/bootstrap',
AWS_LWA_INVOKE_MODE: 'response_stream',
PORT: '8000',
},
})
// PythonFunction generates the handler in Python module format,
// but LWA uses run.sh as the entry point, so override it with L1
const cfnMemoryFunction = memoryFunction.node.defaultChild as lambda.CfnFunction
cfnMemoryFunction.addPropertyOverride('Handler', 'run.sh')
memoryFunction.addToRolePolicy(
new iam.PolicyStatement({
actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'],
resources: ['*'],
}),
)
memory.grantFullAccess(memoryFunction)
// Add POST /invoke-memory endpoint
api.root.addResource('invoke-memory').addMethod(
'POST',
new apigateway.LambdaIntegration(memoryFunction, {
responseTransferMode: apigateway.ResponseTransferMode.STREAM,
timeout: cdk.Duration.minutes(5),
}),
methodOptions,
)
Set the resource ID of the Memory created with CDK to the MEMORY_ID environment variable.
PythonFunction detects the requirements.txt in the directory specified by entry and runs pip install inside a Docker container during cdk synth. Docker also resolves native wheels for ARM64.
Verifying AgentCore Memory Version
After deploying, let's hit the /invoke-memory endpoint.
curl -N -X POST "$API_ENDPOINT/invoke-memory" \
-H "Content-Type: application/json" \
-H "Authorization: $ID_TOKEN" \
-d '{"prompt":"Hello, my name is Kamino. I like TypeScript, and my hobby is visiting supermarkets. Please remember this.","session_id":"ltm-test-1"}'
Hello, Kamino! Nice to meet you.
I will remember the following about you:
- Name: Kamino
- Favorite programming language: TypeScript
- Hobby: Visiting supermarkets
What a wonderful hobby! I will keep this information in mind to be helpful in future conversations.
Confirming memory within the same session works the same as the S3 version.
Wait a moment for the long-term memory extraction to complete, then try calling with a different sessionId.
curl -N -X POST "$API_ENDPOINT/invoke-memory" \
-H "Content-Type: application/json" \
-H "Authorization: $ID_TOKEN" \
-d '{"prompt":"Do you know anything about me?","session_id":"ltm-cross-session"}'
Yes, I know some things about you, Kamino!
- Name: Kamino
- Favorite language: TypeScript
- Hobby: Visiting supermarkets
TypeScript is indeed a wonderful language. It has type safety and improves development efficiency.
Also, I think visiting supermarkets is a wonderful hobby where you can discover unique products and regional food culture from various places!
This is a session ID being used for the first time, but it remembers the information shared in the previous conversation!!
AgentCore Memory was automatically extracting the user's preferences from the conversation behind the scenes, and the following records were saved!
{"context":"The user explicitly stated during self-introduction that they like TypeScript.",
"preference":"Likes TypeScript",
"categories":["Programming","Technology"]}
{"context":"The user explicitly stated during self-introduction that their hobby is visiting supermarkets.",
"preference":"Hobby is visiting supermarkets",
"categories":["Hobbies","Shopping"]}
How to Choose Between AgentCore and Other Options
The Strands Lambda deployment guide explains that the Lambda examples within the guide do not implement streaming, and lists Fargate as an option when it is needed. However, as I tried this time, combining API Gateway's streaming support with Hono allows you to build a streaming API even with Lambda.
If you want to mount it on an existing API infrastructure, make full use of API Gateway features like Cognito, WAF, and usage plans, or reduce costs with per-request billing, then Lambda + API Gateway seems advantageous. I think a minimal version using S3 sessions can be implemented fairly easily. It is possible to combine API Gateway with AgentCore, but since they cannot be natively integrated and a proxy Lambda is required, I felt that was a slight drawback.
Another advantage I felt with Lambda + API Gateway was the freedom of routing. In the AgentCore Runtime service contract, the entry point for an agent is fixed to POST /invocations (only /ping for health checks and /ws for WebSocket cases otherwise). If you want to handle processes with different roles, you need to branch based on the payload within a single endpoint.
With the configuration in this article, you can leverage Hono or FastAPI routing as-is, so in fact this article separates /api/invoke and /api/invoke-memory by path. Since you can co-locate endpoints other than the agent—such as session list retrieval or configuration APIs—in the same web framework, I felt there are advantages when you want to embed an agent as part of a web application's API!
On the other hand, if you need long-running tasks exceeding 15 minutes or session isolation via microVMs, you should consider AgentCore. Using the AgentCore CLI, you can go from agent scaffolding to deployment in one shot, so I think it's easy to get started even when you want to build something working first. I felt it depends on the case, but if you just want to build an AI agent and try it out, trying it with AgentCore Runtime or Harness might be a good idea.
I Also Built a Chat UI as a Bonus
I also built a chat UI for comparison purposes!
It's better to have something full-stack to use, and the chat UI is included in the main branch.

It's a configuration where a React + Vite SPA is served via S3 + CloudFront.

The endpoint selector in the upper left of the screen lets you switch between the S3 SessionManager version and the AgentCore Memory version without leaving the same screen. In the screenshot, when asking "Do you remember anything about me?" with the AgentCore Memory version, it answers with interests automatically extracted from past conversations!
If you switch the sessionId with "New Conversation" and ask the same question, the S3 version forgets while the AgentCore Memory version remembers—you can test the comparison from the main article by clicking around in the UI.
The handler in the main article returned text as-is, but since I wanted to also show the tool call process in the UI, I changed the response to SSE. Since what agent.stream() returns is an internal event object from the SDK, if you only want to stream text, the original approach in the main article is sufficient.
The final directory structure looks like this.
lambda-api-strands/
├── bin/app.ts
├── lib/
│ ├── agent-api-stack.ts # API GW + Lambda + Cognito + CloudFront
│ └── model-catalog.ts # Haiku / Sonnet switching definitions
├── lambda/
│ ├── handler.ts # Hono + S3 SessionManager (SSE)
│ ├── validate-request.ts
│ └── memory/main.py # FastAPI + AgentCore Memory (SSE)
├── frontend/ # React + Vite chat UI
│ ├── public/app-config.json # Cognito / endpoint / model definitions
│ └── src/
│ ├── components/ # Header, ChatMessage, EndpointSelector ...
│ ├── hooks/useChat.ts # Managing send and stream state
│ └── lib/sse.ts # SSE parser (shared between TS and Python versions)
└── package.json
Setup instructions are summarized in the README. Running pnpm deploy flows through everything from frontend build to CDK deployment in one shot, so if you want to try it out, please check there!
If you have any feedback, please let me know via an Issue!
Closing
I wanted to try deploying with Lambda as well, so I gave it a shot. There seem to be cases where you'd choose this kind of configuration instead of AgentCore.
I hope this article is helpful in some way. Thank you for reading to the end!
Supplementary Notes
Notes on API Gateway Response Streaming
When streaming is enabled, some behaviors change. API Gateway calls Lambda with the InvokeWithResponseStream API, so the Lambda side needs to return in a format equivalent to streamifyResponse (Hono handles this internally). The integration timeout can now be extended up to 15 minutes (the default remains 29 seconds), the first 10 MB of the response has no bandwidth limit, and subsequent data is limited to 2 MB/s. Billing is counted in units of 10 MB of response payload.
S3 SessionManager Session Structure
Session data is saved to S3 with this structure. Since the JWT sub is included as a prefix, history is separated per user at the application level.
sessions/<userSub>/session/<sessionId>/
└── scopes/
└── agent/
└── <agentId>/
└── snapshots/
└── snapshot_latest.json
SessionManager overwrites snapshot_latest.json every time an invocation completes. When the next request comes in with the same sessionId, it automatically restores the snapshot and continues the conversation.
How to Delete
Once verification is complete, please delete the entire stack. Since autoDeleteObjects is set for the S3 bucket and RemovalPolicy.DESTROY is set for AgentCore Memory, both will be deleted at the same time as the stack deletion.
npx cdk destroy
