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

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 try it out!

The code for this article is published 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 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.

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, while 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 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.

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

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',
    // 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.

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

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 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 call 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":"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.

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

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 Kamino. My favorite food is curry. Please remember that.","sessionId":"blog-test-1"}'
1st response
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.
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: 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.

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

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

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 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)
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 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.

1st request (self-introduction)
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"}'
1st response
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.

Confirming memory from a different session
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"}'
Response from different 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!

Records automatically extracted by AgentCore Memory
{"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.

https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-http-protocol-contract.html

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.

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

It's a configuration where a React + Vite SPA is served 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 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.

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

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.

Command to run
npx cdk destroy

Share this article

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