I built an API with authentication, streaming, and session management using Strands Agents + Lambda + API Gateway

I built an API with authentication, streaming, and session management using Strands Agents + Lambda + API Gateway

I tried deploying Strands Agents with Lambda + API Gateway, and tested S3 session management and AgentCore Memory integration!
2026.07.25

This page has been translated by machine translation. View original

Introduction

Hello, I'm Jinno 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.

https://strandsagents.com/docs/user-guide/deploy/deploy_to_aws_lambda/

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 give it a try!

The code for this article is publicly available on GitHub.

https://github.com/yuu551/lambda-api-strands

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.

execution command
git clone -b v1 https://github.com/yuu551/lambda-api-strands.git
cd lambda-api-strands
pnpm install
pnpm deploy

Prerequisites

The versions verified 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.

Backend architecture diagram. The client obtains an ID token from Cognito and calls API Gateway; POST /api/invoke connects to a Node.js Lambda and S3 session bucket, POST /api/invoke-memory connects to a Python Lambda and AgentCore Memory. Both Lambdas call Amazon Bedrock

In the first half, we will only build ① the S3 SessionManager version. The configuration is simple, with just 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, attaches it to the Authorization header, and calls the API.
Since API Gateway's Cognito authorizer validates the token, 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.

https://dev.classmethod.jp/articles/shuntaka-cdk-hono-lambda-web-adapter-apigateway-15/

Implementation

This time I implemented it with the following structure! It turned out 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.

execution command
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.

lambda/handler.ts
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',
    // Separate 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 the API Gateway authorizer validates the token, it decodes the contents and puts them in the Lambda event. This means the Lambda side doesn't need to parse the token itself, and can obtain information about the logged-in user simply by reading c.env.event.requestContext.authorizer.claims.sub.

By embedding this sub at the beginning of the S3 key, the storage location is divided into sessions/{sub}/{sessionId}/..., so even if someone specifies the same sessionId as another person, the path will be different if the sub is different, preventing conversation histories from getting mixed up.

Streaming is handled inside Hono's streamText().
Events have a two-layer structure, with modelContentBlockDeltaEvent inside the outer modelStreamUpdateEvent, and items where delta.type is textDelta are the text increments. Passing these to stream.write() returns them to the client sequentially.

Session management can be achieved by setting S3Storage in the SessionManager. Sessions are identified by the sessionId in the request body, and with the same sessionId, the previous conversation history is automatically restored. No DynamoDB table design or custom session management code is needed. It's easy to implement, which is great.

CDK Stack

Here is the main stack definition.

lib/agent-api-stack.ts
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 NodejsFunction is used, handler.ts is automatically bundled with esbuild. Since the Strands SDK, Hono, and zod are all included in the bundle, no Lambda layers are 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 compatible with Lambda streaming
architecture ARM64
responseTransferMode STREAM Enables API Gateway response streaming
timeout 5 minutes Lambda function timeout. Set longer to accommodate long responses
bundling.format ESM Bundle format for NodejsFunction

Deployment

Once implemented, let's try deploying.

execution command
pnpm install
npx cdk deploy
execution result
  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.

execution command
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 it without a token.

execution command
curl -X POST "$API_ENDPOINT/invoke" \
  -H "Content-Type: application/json" \
  -d '{"prompt": "Hello"}'
execution result
{"message":"Unauthorized"}

It's properly rejected with a 401!

Verifying Streaming

Obtain a token from Cognito and try calling with streaming.

execution command
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":"List 3 AWS services you like","sessionId":"test-1"}'

Characters were displayed sequentially in the terminal! When I measured the timing of chunk arrivals to verify whether it was truly streaming, the results were as follows.

execution result (excerpt of chunk arrival times)
[+  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 about 1.5 seconds after the request, with 74 chunks arriving sequentially!

Verifying Multi-turn Conversation

Let's verify that the S3 SessionManager is working. Try calling twice with the same sessionId.

1st call (self-introduction)
curl -N -X POST "$API_ENDPOINT/invoke" \
  -H "Content-Type: application/json" \
  -H "Authorization: $ID_TOKEN" \
  -d '{"prompt":"Hello, I am Jinno. My favorite food is curry. Please remember that.","sessionId":"blog-test-1"}'
1st response
Hello, Jinno! 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 during our future conversations.
2nd call (memory check)
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"}'
2nd response
Yes, I remember!

Your name: Jinno
Favorite food: Curry

That's right. I will make use of this information in our future questions and conversations!

Oh, it's properly responding based on the previous conversation!

Looking at the contents of S3, the paths were separated by user based on the Cognito sub.

execution result
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 another user calls with the same sessionId, the path changes, so conversation histories are separated per user at the application level.

Lambda is stateless, but multi-turn conversations are achieved thanks to the S3 SessionManager.
The S3 version is working end-to-end now!!

Also Trying the AgentCore Memory Version

Using the SessionManager for AgentCore Memory, you can also easily integrate long-term memory that automatically extracts user preferences and factual information from conversations and makes them available across different sessions.

However, AgentCoreMemorySessionManager for Strands is only available for Python at the time of writing. For that reason, I added a separate Python Lambda using FastAPI + Lambda Web Adapter in addition to the Hono (TypeScript) one. Only this part changes languages, 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.

lib/agent-api-stack.ts (additions)
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 Cognito's sub as actor_id on the Python side later, memory is separated per user.

Python Lambda Handler

Here is the handler for FastAPI + Strands Python SDK + AgentCore Memory SessionManager.

lambda/memory/main.py
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 it cannot be obtained (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 do not 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 flushing 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 a few differences.

For streaming, agent.stream_async() is used. It is an asynchronous generator equivalent to TypeScript's agent.stream(), with text increments coming in the data key of events.

For user isolation, simply pass Cognito's 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's streaming mode, API Gateway's requestContext is passed as JSON in the x-amzn-request-context header. With Hono, you could extract the event via c.env.event, but with FastAPI via LWA, you extract it from the request header.

Adding to CDK

Add a Python Lambda to the CDK stack. Using PythonFunction from @aws-cdk/aws-lambda-python-alpha automatically bundles 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)
lib/agent-api-stack.ts(additions)
    // 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 in 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.

Testing the AgentCore Memory Version

After deploying, let's hit the /invoke-memory endpoint.

1st request (self-introduction)
curl -N -X POST "$API_ENDPOINT/invoke-memory" \
  -H "Content-Type: application/json" \
  -H "Authorization: $ID_TOKEN" \
  -d '{"prompt":"こんにちは、私は神野です。TypeScriptが好きで、趣味はスーパーマーケット巡りです。覚えてね。","session_id":"ltm-test-1"}'
1st response
Hello, Kamino-san! Nice to meet you.

I will remember the following about you:
- Name: Kamino-san
- Favorite programming language: TypeScript
- Hobby: Visiting supermarkets

What a wonderful hobby! I will keep this information in mind to be helpful in future conversations.

Verifying memory within the same session works the same as with the S3 version.
Wait a moment for the long-term memory extraction to finish, then call it with a different sessionId.

Checking memory from a different session
curl -N -X POST "$API_ENDPOINT/invoke-memory" \
  -H "Content-Type: application/json" \
  -H "Authorization: $ID_TOKEN" \
  -d '{"prompt":"私について何か知ってることある?","session_id":"ltm-cross-session"}'
Response from the different session
Yes, I know some things about you, Kamino-san!

- Name: Kamino-san
- Favorite language: TypeScript
- Hobby: Visiting supermarkets

TypeScript is indeed a great language. It has type safety and good development efficiency.
Also, I think visiting supermarkets is a wonderful hobby where you can discover unique products and local food culture from various places!

It's a session ID being used for the first time, yet it remembers the information shared in the previous conversation!!

AgentCore Memory was automatically extracting the user's preferences from the conversation in the background, and records like the following were saved!

Records automatically extracted by AgentCore Memory
{"context":"The user explicitly stated in their self-introduction that they like TypeScript.",
 "preference":"Likes TypeScript",
 "categories":["Programming","Technology"]}

{"context":"The user explicitly stated in their 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 states that the Lambda example in the guide does not implement streaming, and lists Fargate as an option when streaming is needed. However, as I tested this time, combining API Gateway's streaming support with Hono allows you to build a streaming API even with Lambda.

If you want to integrate with an existing API infrastructure, use the full features of API Gateway such as Cognito, WAF, and usage plans, or keep costs down with per-request billing, Lambda + API Gateway seems advantageous. The minimal version using S3 sessions feels like it can be implemented relatively easily. It's also possible to combine API Gateway with AgentCore, but since they can't be natively integrated and a proxy Lambda is required, I felt that was a slight drawback.

On the other hand, if you need long-running tasks that exceed 15 minutes or session isolation via microVMs, you should consider AgentCore. Using the AgentCore CLI, you can go all the way from scaffolding an agent to deployment in one go, so I think it's also approachable for cases where you just want to get something working first. It felt like it depends on the case, but if you just want to build an AI agent and try it out, it might be worth trying AgentCore Runtime or Harness.

I Also Built a Chat UI as a Bonus

I went ahead and built a comparison chat UI as well!
It's better to be able to use it full-stack, and the chat UI is included in the main branch.

Chat screen of Strands Lambda Chat. When asking "Do you remember me?" on the AgentCore Memory endpoint, it answers with interests extracted from past conversations

It's a configuration that delivers a React + Vite SPA via S3 + CloudFront.

Architecture diagram including the SPA. The React SPA in the browser signs in directly with Cognito and retrieves static assets from S3 via CloudFront. /api/* is forwarded to API Gateway, and two Lambdas use the S3 session bucket and AgentCore Memory respectively

The endpoint selector in the upper left of the screen lets you switch between the S3 SessionManager version and the AgentCore Memory version on the same screen. In the screenshot, when asking "Do you remember me?" on 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, you can click through the comparison from the main content in the UI — the S3 version forgets while the AgentCore Memory version remembers.

The handler in the main content returned text as-is, but since I wanted to show the tool call process in the UI, I changed the response to SSE. Since what agent.stream() returns is an internal event object of the SDK, the main content as-is is sufficient if you just want to stream text.

The final directory structure ended up like this.

lambda-api-strands/
├── bin/app.ts
├── lib/
│   ├── agent-api-stack.ts       # API GW + Lambda + Cognito + CloudFront
│   └── model-catalog.ts         # Haiku / Sonnet switching definition
├── 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     # Manages sending and stream state
│       └── lib/sse.ts           # SSE parser (shared between TS and Python versions)
└── package.json

The setup instructions are summarized in the README. pnpm deploy runs everything in one go from building the frontend to CDK deployment, so if you want to try it out, please refer to that!
If you have any feedback, please let me know via an Issue!

Closing

I wanted to try deploying on Lambda too, so I gave it a shot. It seems like there are cases where you might choose this kind of configuration rather than just AgentCore.

I hope this article was 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 using 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 per 10 MB unit of response payload.

https://docs.aws.amazon.com/apigateway/latest/developerguide/response-transfer-mode-lambda.html

Session Structure of S3 SessionManager

Session data is saved in 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 on the S3 bucket and RemovalPolicy.DESTROY is set on AgentCore Memory, both will be deleted along with the stack deletion.

Command to run
npx cdk destroy

Share this article

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