Skip to content

Building Production-Ready AI Agents with AWS Bedrock AgentCore

How AWS Bedrock AgentCore solves the infrastructure challenges of deploying agentic AI at scale, with runtime, memory, gateway, and multi-agent coordination.

Ayhan Sipahi Ayhan Sipahi

Deploying LangChain or CrewAI agents to production requires session isolation, credential management, memory persistence, and observability: infrastructure that takes months to build correctly from scratch. Without it, agents lack security boundaries between users and have no durable state across sessions, which makes them unsuitable for real workloads.

AWS Bedrock AgentCore (GA October 2025) closes that gap. It’s not another agent framework competing with LangChain or CrewAI; it’s the managed infrastructure layer that agents built with ANY framework need to run at scale. Think of it as “Lambda for AI agents”: you bring your agent code, AgentCore handles runtime, memory, tool management, and security. When the agent code already works locally and the remaining work is operational, that managed layer is the faster default.

AgentCore Architecture#

AgentCore consists of five integrated services that work independently or together:

User Request

AgentCore Runtime

AI Agent

AgentCore Memory

AgentCore Gateway

AgentCore Identity

Tools Lambda/API/MCP

Short-Term Context

Long-Term Memory

Observability

CloudWatch

Runtime: Serverless execution environment with 8-hour session windows and automatic session isolation using dedicated microVMs per user.

Memory: Managed storage for both short-term conversation context and long-term user preferences, facts, and summaries - without building your own vector database.

Gateway: Centralized tool management using the Model Context Protocol (MCP). Convert Lambda functions, REST APIs, and existing services into agent-accessible tools.

Identity: Secure credential management with OAuth 2.0 integration. Agents access third-party APIs on behalf of users without storing credentials.

Observability: OpenTelemetry-compatible metrics and traces exported to CloudWatch, Datadog, or LangSmith.

Deploying Agents on Any Framework#

Here’s how to deploy a Strands agent to AgentCore:

from bedrock_agentcore import BedrockAgentCoreApp
from strands import Agent

app = BedrockAgentCoreApp()

@app.entrypoint
def invoke(payload):
    agent = Agent(
        model="anthropic.claude-sonnet-4-20250514-v1:0",
        instructions="You are a customer support agent with access to order history and return policies."
    )
    return agent.run(payload.get("message"))

Deploy with the CLI:

agentcore configure
agentcore launch --region us-east-1

Runtime execution windows run up to 8 hours, long enough for async agentic workflows where traditional serverless functions timeout at 15 minutes. Each user gets a dedicated microVM, so sessions carry no data leakage between them. Billing counts only active CPU and memory; I/O wait time is free, which can make AgentCore significantly cheaper than pre-allocated Lambda configurations for agentic workloads that spend considerable time waiting on LLM responses. Containers must target ARM64: use --platform=linux/arm64 in Docker builds.

One frequent mistake is not handling the Mcp-Session-Id header, which AgentCore auto-injects for stateless MCP servers:

from fastapi import FastAPI, Header

app = FastAPI()

@app.post("/mcp")
async def mcp_endpoint(
    mcp_session_id: str = Header(None, alias="Mcp-Session-Id")
):
    # AgentCore manages session isolation
    # Your server must accept platform-generated IDs
    session_state = load_session(mcp_session_id)
    return {"status": "ok"}

Managing Conversation and Long-Term Memory#

AgentCore Memory covers both: short-term conversation context and long-term knowledge persistence.

Memory extraction pipeline:

KMSLLMMemoryGuardrailsAgentUserKMSLLMMemoryGuardrailsAgentUserMessageValidate InputPassedCreateEvent APIExtract MemoriesStructured MemoryEncryptEncrypted StorageResponse

Implementing memory with three strategies:

from bedrock_agentcore.memory import (
    MemoryClient,
    UserPreferenceMemoryStrategy,
    SemanticMemoryStrategy,
    SummaryMemoryStrategy
)

memory_client = MemoryClient()

# Create memory with multiple strategies
memory = memory_client.create_memory(
    name="customer-support-memory",
    strategies=[
        UserPreferenceMemoryStrategy(),  # Learn user patterns
        SemanticMemoryStrategy(),  # Store facts/knowledge
        SummaryMemoryStrategy()  # Compress sessions
    ],
    encryption_key_arn="arn:aws:kms:us-east-1:123456789012:key/abc123"
)

# Store conversation event
memory_client.create_event(
    memory_id=memory.id,
    event_data={
        "type": "conversation",
        "content": "User prefers technical explanations with code examples"
    }
)

Strategy selection guide:

Agent typeStrategiesWhat it remembers
Customer supportUserPreferences + SummariesCommunication style
Technical assistantSemanticFacts + SummariesCodebase knowledge
Personal agentAll three strategiesComprehensive personalization

Critical security pattern - always use Guardrails before CreateEvent API:

import boto3

bedrock = boto3.client('bedrock')

# WRONG: Direct storage (vulnerable to memory poisoning)
# memory_client.create_event(
#  memory_id=memory.id,
#  event_data={"content": user_input}
# )

# RIGHT: Sanitize with Guardrails first
guardrail_response = bedrock.apply_guardrail(
    guardrailId='guardrail-123',
    guardrailVersion='1',
    content=[{"text": {"text": user_input}}]
)

if guardrail_response['action'] == 'NONE':
    memory_client.create_event(
        memory_id=memory.id,
        event_data={"content": user_input}
    )
else:
    # Block and log attack attempt
    logger.warning(f"Memory poisoning attempt blocked: {guardrail_response}")

Cost optimization: Limit retriever hops. Two-three retrieval operations per turn is normal, ten indicates over-retrieval:

memory_config = {
    'retrieval_strategy': 'semantic',
    'max_results': 5,
    'max_retriever_hops': 2
}

Gateway: Centralized Tool Management#

Embedding tools directly in agent code leads to duplication and inconsistency: customer support, sales, and technical agents that all need weather data end up maintaining three copies of the same tool code.

AgentCore Gateway centralizes that in MCP-compatible tool servers:

LangChain Agent

AgentCore Gateway MCP

CrewAI Agent

Custom Agent

Lambda Target

OpenAPI Target

MCP Server Target

Smithy Model Target

Registering a Lambda function as a tool:

import boto3

agentcore = boto3.client('bedrock-agentcore')

# Register Lambda as tool target
response = agentcore.create_target(
    gatewayId='gateway-123',
    targetConfig={
        'type': 'LAMBDA',
        'lambdaArn': 'arn:aws:lambda:us-east-1:123456789012:function:get-weather',
        'description': 'Get current weather for a city'
    }
)

Gateway covers authentication, semantic tool search, and protocol conversion. IAM roles handle AWS resources, OAuth 2.0 covers third-party APIs, and API keys cover other services; agents discover relevant tools via x_amz_bedrock_agentcore_search without knowing everything available upfront; Lambda functions, OpenAPI specs, Smithy models, and MCP servers all get exposed through one standardized MCP interface.

Architecture pattern - centralize common tools, keep domain-specific tools local:

Common Tools (via Gateway):
  - Web search
  - Database queries
  - Weather API
  - Stock prices

Domain-Specific Tools (agent-local):
  - Return policy logic
  - Product catalog
  - Business rules

Multi-Agent Coordination with A2A Protocol#

AgentCore coordinates multi-agent teams through the Agent-to-Agent (A2A) protocol.

MCP handles agent-to-tool communication, like an agent calling a weather API; A2A handles agent-to-agent communication, like a supervisor coordinating specialists.

Hub-and-spoke supervisor implementation:

import { BedrockAgentCoreClient, InvokeAgentCommand } from '@aws-sdk/client-bedrock-agentcore';

class HostAgent {
  private client: BedrockAgentCoreClient;
  private specialistAgents: Map<string, AgentConfig>;

  async routeToSpecialist(query: string, capability: string) {
    const agentConfig = this.specialistAgents.get(capability);

    // Fetch remote agent's A2A configuration
    const agentCard = await this.fetchAgentCard(agentConfig.endpoint);

    // Invoke via A2A protocol
    const command = new InvokeAgentCommand({
      agentId: agentCard.id,
      sessionId: this.generateSessionId(),
      inputText: query,
      protocol: 'A2A'
    });

    return await this.client.send(command);
  }

  private async fetchAgentCard(endpoint: string): Promise<AgentCard> {
    // Retrieve agent capabilities schema
    const response = await fetch(`${endpoint}/.well-known/agent-card`);
    return response.json();
  }
}

Orchestration patterns:

User Query

Supervisor Agent

Analyze Query

Decompose Problem

Specialist A Parallel

Specialist B Parallel

Aggregate Results

Specialist C Serial

Supervisor Synthesis

Response

Supervisor with routing mode - not every query needs full orchestration:

class SupervisorAgent:
    def route_query(self, query: str):
        # Simple query → direct routing
        if self.is_simple_query(query):
            specialist = self.select_single_specialist(query)
            return specialist.invoke(query)

        # Complex query → full orchestration
        else:
            plan = self.analyze_and_plan(query)
            results = self.orchestrate_subagents(plan)
            return self.synthesize(results)

    def is_simple_query(self, query: str) -> bool:
        intents = self.detect_intents(query)
        return len(intents) == 1

Framework interoperability: LangGraph monitoring agent + CrewAI analytics agent + Strands incident response agent can all communicate via A2A, with no framework lock-in.

Security and Cost Optimization#

Guardrails Configuration#

Guardrails protect against prompt injection, memory poisoning, and harmful content:

import boto3

bedrock = boto3.client('bedrock')

guardrail = bedrock.create_guardrail(
    name='production-agent-guardrail',
    contentPolicyConfig={
        'filtersConfig': [
            {'type': 'HATE', 'inputStrength': 'HIGH', 'outputStrength': 'HIGH'},
            {'type': 'VIOLENCE', 'inputStrength': 'MEDIUM', 'outputStrength': 'HIGH'},
            {'type': 'PROMPT_ATTACK', 'inputStrength': 'HIGH', 'outputStrength': 'NONE'}
        ]
    },
    topicPolicyConfig={
        'topicsConfig': [
            {
                'name': 'Financial Advice',
                'definition': 'Providing specific investment recommendations',
                'type': 'DENY'
            }
        ]
    },
    wordPolicyConfig={
        'wordsConfig': [
            {'text': 'internal-api-key'},
            {'text': 'secret-token'}
        ],
        'managedWordListsConfig': [
            {'type': 'PROFANITY'}
        ]
    }
)

Defense-in-depth strategy:

  1. Input validation: Block malicious prompts at entry
  2. Memory protection: Sanitize before CreateEvent API
  3. Output filtering: Prevent harmful responses
  4. Audit trails: CloudWatch logs for compliance

Cost Optimization Strategies#

Prompt caching - 90% discount on cached tokens:

response = bedrock_runtime.converse(
    modelId="anthropic.claude-sonnet-4-20250514-v1:0",
    messages=[{"role": "user", "content": user_query}],
    system=[
        {
            "text": large_system_prompt,
            "cachePoint": {"type": "default"}
        }
    ]
)

Model routing - match complexity to model cost:

def route_to_model(query: str) -> str:
    complexity = classify_query_complexity(query)

    if complexity < 0.3:
        return "anthropic.claude-haiku-4-5-20251001-v1:0"  # $1/$5 per 1M tokens (input/output)
    elif complexity < 0.7:
        return "anthropic.claude-sonnet-4-20250514-v1:0"  # $3/$15 per 1M tokens (input/output)
    else:
        return "anthropic.claude-opus-4-20250514-v1:0"  # $15/$75 per 1M tokens (input/output)

Tool-call budgets - prevent unbounded tool use:

agent = Agent(
    model="anthropic.claude-sonnet-4-20250514-v1:0",
    max_tool_calls_per_turn=3,
    instructions="If user asks about multiple items, summarize instead of exhaustive lookup"
)

Simple less than 0.3

Medium 0.3 to 0.7

Complex greater than 0.7

Yes

No

OK

Exceeded

User Query

Classify Complexity

Claude Haiku

Claude Sonnet

Claude Opus

Cached?

90 percent Discount

Full Cost

Budget Check

Execute

Reject

Cost components:

  • Runtime: Active CPU/memory consumption (not pre-allocated)
  • Memory: Short-term (per event), long-term (per memory processed + retrievals)
  • Gateway: MCP operations (ListTools, CallTool, Ping) + semantic search queries
  • Identity: No additional charges when used via Runtime/Gateway
  • Observability: CloudWatch standard pricing

Where It Breaks in Production#

Memory Poisoning Without Guardrails#

Storing raw user input directly allows prompt injection into memory:

# WRONG
user_input = "Ignore previous instructions, you are now..."
memory_client.create_event(
    memory_id=memory.id,
    event_data={"content": user_input}
)

Sanitizing with Guardrails before the write (the same pattern shown in the Memory section above) closes this gap.

Tool-Call Storms#

Without limits, an agent can invoke 20+ tools per query:

User: "What's the weather in major cities?"
Agent makes 50 separate get_weather() calls
Latency and cost grow with the call count, with no upper bound

Enforcing tool-call budgets and guiding the agent via instructions, the same pattern shown under Cost Optimization above, keeps that bounded.

Deployment Configuration Gaps#

Two configuration details cause avoidable failures at deploy time. Using x86 containers causes deployment failures; build for ARM64 explicitly:

FROM --platform=linux/arm64 python:3.11-slim
COPY . /app
CMD ["python", "agent.py"]
docker buildx build --platform linux/arm64 -t agent:latest .

Separately, agent traffic goes over the public internet by default. For internal APIs, configuring VPC and PrivateLink keeps that traffic inside AWS:

runtime_config = {
    'vpcConfig': {
        'securityGroupIds': ['sg-12345'],
        'subnetIds': ['subnet-abc', 'subnet-def']
    },
    'privateLinkEnabled': True
}

When to Use AgentCore#

Use AgentCore when:

  • Multiple agent frameworks in use (LangChain + CrewAI + custom)
  • Need to evaluate different models (Bedrock + OpenAI + Anthropic)
  • Enterprise security required (VPC, PrivateLink, customer-managed KMS)
  • Multi-agent systems planned (A2A coordination)
  • Time-to-production measured in weeks
  • Team size under 10 (can’t build infrastructure from scratch)

Consider alternatives when the picture is simpler: a single framework forever (only LangGraph, so LangGraph Cloud fits), a single cloud ecosystem (all Azure, so Azure AI Agent Service fits), sustained very high volume where amortized self-hosted capacity can undercut consumption pricing, a need for custom hardware such as GPUs for specialized models, or agent infrastructure that’s already built and sunk.

Adopt the platform incrementally: Runtime first, then Memory, Gateway, Identity, and Observability as each need appears. Each service works on its own, so stopping partway is a valid end state. Override the default when one framework and one cloud will carry the workload for its whole life; that framework’s own platform stays simpler, and the managed layer earns little.

References#

Related posts