<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: AWS Community Builders </title>
    <description>The latest articles on DEV Community by AWS Community Builders  (aws-builders).</description>
    <link>https://dev.to/aws-builders</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F2794%2F88da75b6-aadd-4ea1-8083-ae2dfca8be94.png</url>
      <title>DEV Community: AWS Community Builders </title>
      <link>https://dev.to/aws-builders</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aws-builders"/>
    <language>en</language>
    <item>
      <title>Hands-On with the New Amazon CloudFront Support for AWS Lambda Web Functions</title>
      <dc:creator>Irfan Satrio</dc:creator>
      <pubDate>Sun, 18 Oct 2026 12:41:41 +0000</pubDate>
      <link>https://dev.to/aws-builders/hands-on-with-the-new-amazon-cloudfront-support-for-aws-lambda-web-functions-7i9</link>
      <guid>https://dev.to/aws-builders/hands-on-with-the-new-amazon-cloudfront-support-for-aws-lambda-web-functions-7i9</guid>
      <description>&lt;p&gt;On October 12, 2026, Amazon Web Services introduced &lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/10/cloudfront-lambda-web-functions-origin/" rel="noopener noreferrer"&gt;support for using AWS Lambda Web Functions as origins for Amazon CloudFront&lt;/a&gt;. This capability allows web applications running on Lambda Web Functions to be delivered through CloudFront's global edge network without requiring changes to application code.&lt;/p&gt;

&lt;p&gt;By placing CloudFront in front of a Lambda Web Function, applications can benefit from caching, custom domains with managed TLS certificates, and additional security through AWS WAF. CloudFront Origin Access Control (OAC) can also restrict access to the Lambda Web Function so that requests reach the origin through CloudFront.&lt;/p&gt;

&lt;p&gt;In this post, I explored the new integration by configuring a Lambda Web Function as a CloudFront origin and verifying the setup using HTTP requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;To test the new feature, I created a Lambda Web Function in ap-southeast-1 using Node.js 24.x and the default Hello World template. For CloudFront OAC integration, the endpoint must use IAM authentication (IamAuth). I configured an endpoint named web-function-endpoint with the Home-Region endpoint type, IAM authentication enabled, and automatic deployment enabled.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Focdz0cthd8fk8m4pqrle.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Focdz0cthd8fk8m4pqrle.png" alt=" " width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Home-Region endpoint is a regional endpoint for the Lambda Web Function. The endpoint type is selected during creation and cannot be changed afterward. With IamAuth enabled, requests to the endpoint must be authenticated before the endpoint can be invoked.&lt;/p&gt;

&lt;p&gt;I then created a CloudFront distribution and configured the Lambda Web Function endpoint as its origin. Under Origin access, I enabled Allow private Lambda function access to CloudFront to use Origin Access Control (OAC).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fryt9g8c3m5kh5put0rdc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fryt9g8c3m5kh5put0rdc.png" alt=" " width="800" height="122"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To allow CloudFront to invoke the endpoint, I configured a resource-based policy on the Lambda Web Function. The policy grants the CloudFront service principal permission to invoke the endpoint and restricts access to the specific distribution using &lt;code&gt;aws:SourceArn&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Example policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AllowCloudFrontOACAccess"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cloudfront.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lambda:InvokeWebFunctionEndpoint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:lambda:ap-southeast-1:&amp;lt;ACCOUNT_ID&amp;gt;:web-function/lab-web-function/endpoint/web-function-endpoint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"aws:SourceArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:cloudfront::&amp;lt;ACCOUNT_ID&amp;gt;:distribution/&amp;lt;DISTRIBUTION_ID&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I applied the policy using the AWS CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lambda-web put-resource-policy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-arn&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:lambda:ap-southeast-1:&amp;lt;ACCOUNT_ID&amp;gt;:web-function/lab-web-function"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy&lt;/span&gt; file://policy.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-southeast-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;&amp;lt;ACCOUNT_ID&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;DISTRIBUTION_ID&amp;gt;&lt;/code&gt; with your actual values. Note that the policy uses the endpoint ARN, while the CLI command uses the Lambda Web Function ARN.&lt;/p&gt;

&lt;p&gt;According to AWS documentation, several configuration details are important when using a Lambda Web Function as a CloudFront origin with OAC:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IAM authentication:&lt;/strong&gt; The endpoint must use &lt;code&gt;IamAuth&lt;/code&gt; so that requests are authenticated and authorized through the Lambda Web Function's resource-based policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OAC signing protocol:&lt;/strong&gt; &lt;code&gt;sigv4&lt;/code&gt; for regional endpoints (Home-Region and Per-Region) and &lt;code&gt;sigv4a&lt;/code&gt; for global endpoints (Multi-Region).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signing behavior:&lt;/strong&gt; Lambda Web Function origins support &lt;code&gt;always-amz-auth&lt;/code&gt; (Always sign, preserve viewer authorization), which signs origin requests while preserving the viewer's original &lt;code&gt;Authorization&lt;/code&gt; header.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Origin request policy:&lt;/strong&gt; If you use an origin request policy, it must exclude the viewer's Host header. If you use a managed policy, use AllViewerExceptHostHeader instead of AllViewer to avoid 404 responses caused by forwarding the viewer's Host header.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization header:&lt;/strong&gt; If the application uses the viewer's &lt;code&gt;Authorization&lt;/code&gt; header, include it in the cache policy for every cache behavior that uses the origin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global endpoints:&lt;/strong&gt; OAC does not support global endpoints that include opt-in Regions. All associated Regions must be enabled by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda@Edge:&lt;/strong&gt; Origin request and origin response triggers are not supported on cache behaviors that use a Lambda Web Function origin with OAC.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;p&gt;To verify the configuration, I first sent a request directly to the Lambda Web Function endpoint using &lt;code&gt;curl&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; https://&amp;lt;LAMBDA_WEB_FUNCTION_ENDPOINT&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0dpk2x41s1czvq6c8pej.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0dpk2x41s1czvq6c8pej.png" alt=" " width="453" height="112"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;403 Forbidden&lt;/code&gt; response is consistent with the endpoint requiring IAM-signed requests. The direct request did not return the default application response.&lt;/p&gt;

&lt;p&gt;I then sent a request through the CloudFront distribution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; https://&amp;lt;CLOUDFRONT_DOMAIN&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjid7ifgl4s4ka7ewhm0k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjid7ifgl4s4ka7ewhm0k.png" alt=" " width="798" height="196"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The successful &lt;code&gt;200 OK&lt;/code&gt; response confirms that the request through CloudFront received a successful response from the configured origin. The response also included &lt;code&gt;X-Cache: Miss from cloudfront&lt;/code&gt;, consistent with the managed CachingDisabled policy used during testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analysis
&lt;/h2&gt;

&lt;p&gt;From testing, direct requests to the Lambda Web Function endpoint returned &lt;code&gt;403 Forbidden&lt;/code&gt;, while requests through CloudFront returned &lt;code&gt;200 OK&lt;/code&gt;. These results are consistent with OAC restricting the origin to requests from the configured CloudFront distribution through signed origin requests and the resource-based policy.&lt;/p&gt;

&lt;p&gt;This integration allows serverless web applications running on Lambda Web Functions to use CloudFront's content delivery and security capabilities while restricting direct access to the origin through Origin Access Control (OAC).&lt;/p&gt;

&lt;p&gt;Based on the capabilities AWS describes, this setup suits workloads such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Serverless web applications that need custom domains and managed TLS certificates&lt;/li&gt;
&lt;li&gt;Web applications that benefit from CloudFront caching&lt;/li&gt;
&lt;li&gt;Applications that need additional application-layer protection through AWS WAF&lt;/li&gt;
&lt;li&gt;Applications that require controlled access to the origin through OAC&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;According to AWS, OAC restricts access to the Lambda Web Function so that it is reachable only through CloudFront. This allows CloudFront to serve as the entry point while keeping direct access to the origin restricted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Based on this hands-on test, Amazon CloudFront support for AWS Lambda Web Functions allows CloudFront to serve as the entry point for serverless web applications while restricting direct access to the origin through Origin Access Control (OAC). The successful HTTP response through CloudFront and the rejected direct endpoint request match the expected behavior of the configured setup.&lt;/p&gt;

&lt;p&gt;For applications built on Lambda Web Functions, this integration provides a way to use CloudFront's delivery and security capabilities without requiring changes to application code.&lt;/p&gt;

&lt;p&gt;CloudFront support for AWS Lambda Web Functions is available worldwide except in the CloudFront China Regions, with no additional fees for the integration.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudfront</category>
      <category>lambda</category>
      <category>security</category>
    </item>
    <item>
      <title>An Agent That Remembers: Short-Term and Long-Term Memory on AgentCore</title>
      <dc:creator>Pin Xiong</dc:creator>
      <pubDate>Sun, 18 Oct 2026 04:51:01 +0000</pubDate>
      <link>https://dev.to/aws-builders/an-agent-that-remembers-short-term-and-long-term-memory-on-agentcore-3bf5</link>
      <guid>https://dev.to/aws-builders/an-agent-that-remembers-short-term-and-long-term-memory-on-agentcore-3bf5</guid>
      <description>&lt;p&gt;An agent that forgets when the reply finishes is the right default and the wrong behaviour for a reorder assistant. This article adds memory to one and proves it three ways: a preference survives into a new session, three managers never see each other's memory, and a failed restock is quoted back seventeen minutes later. Every number was measured in one account, in &lt;code&gt;us-east-1&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The use case
&lt;/h2&gt;

&lt;p&gt;Northwind Gear runs forty stores and restocks them every Monday. A store manager told the reorder assistant, once, that only the North China region matters to them. The next Monday, in a new session, the assistant asked which region again. Nothing was wrong with the model: the sentence never left the session it was said in.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AgentCore does here
&lt;/h2&gt;

&lt;p&gt;Three of the nine capabilities carry this use case.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Memory: one resource holds both halves. Short term is an append-only event log keyed by actor and session; long term is what the service extracts from those events into namespaces. This article goes deep on that split.&lt;/li&gt;
&lt;li&gt;Runtime: hosts &lt;code&gt;app/aia_uc02/&lt;/code&gt; as a managed endpoint, one microVM per session, which is why the session id matters.&lt;/li&gt;
&lt;li&gt;Identity, lightly: the actor id becomes the partition key of the namespace and travels as &lt;code&gt;--user-id&lt;/code&gt;. No Cognito pool here.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The other six stay off: no Gateway, no Policy, no Evaluations, no Code Interpreter, no Browser, and no Observability beyond the traces the runtime emits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 20 or newer (22.22.2 here), the AgentCore CLI 0.31.1, AWS CLI 2.x, uv 0.11.9&lt;/li&gt;
&lt;li&gt;Python 3.10 to 3.14 on the host; the runtime target is PYTHON_3_13, set in the contract&lt;/li&gt;
&lt;li&gt;An AWS account with Bedrock model access; region &lt;code&gt;us-east-1&lt;/code&gt; for everything&lt;/li&gt;
&lt;li&gt;Model &lt;code&gt;deepseek.v3.2&lt;/code&gt;, passed to the code through &lt;code&gt;MODEL_ID&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;IAM permissions to create a runtime, its role, and a memory resource with an execution role; the CLI writes both roles&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/pinxiong/bedrock-agentcore-in-action/tree/master/usecases/02-memory" rel="noopener noreferrer"&gt;https://github.com/pinxiong/bedrock-agentcore-in-action/tree/master/usecases/02-memory&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;02-memory/
├── agentcore/
│   ├── agentcore.json         # the runtime plus one memory with three strategies
│   └── aws-targets.json       # account 123456789012, region us-east-1
├── app/aia_uc02/
│   ├── main.py                # the agent: prompt, tool, actor resolution, entrypoint
│   ├── memory.py              # the only file that knows short term from long term
│   ├── model/load.py          # MODEL_ID -&amp;gt; BedrockModel
│   └── pyproject.toml         # dependencies packaged into the zip
└── tests/
    └── test_sanitize.py       # offline unit tests for the sanitizer, no AWS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;memory.py&lt;/code&gt; is the subject of the article; &lt;code&gt;agentcore.json&lt;/code&gt; decides the namespace layout.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The memory resource is declared once, and the strategies decide what long-term records exist at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"memories"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aia_uc02_mem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eventExpiryDuration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"strategies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USER_PREFERENCE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"namespaceTemplates"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/users/{actorId}/preferences"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EPISODIC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nl"&gt;"namespaceTemplates"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/episodes/{actorId}/{sessionId}"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
                                &lt;/span&gt;&lt;span class="nl"&gt;"reflectionNamespaceTemplates"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/episodes/{actorId}"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SEMANTIC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nl"&gt;"namespaceTemplates"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/users/{actorId}/facts"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actor id sits inside the namespace, so isolation is a property of the key space rather than a filter some code path has to remember. The reflection namespace sits one level up, so a prefix read of &lt;code&gt;/episodes/{actorId}/&lt;/code&gt; reaches a single episode and the lessons written across all of them.&lt;/p&gt;

&lt;p&gt;The agent never stores a preference itself. It appends the turn and asks a question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;remember&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;turns&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# ... the real one wraps this in try/except and returns None on failure
&lt;/span&gt;    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;memory_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actor_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;turns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eventId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;recall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# ... condensed: the real one also drops records whose text is empty,
&lt;/span&gt;    &lt;span class="c1"&gt;# and continues past a namespace that failed instead of falling through
&lt;/span&gt;    &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;by_path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;preference&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/users/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/preferences&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                               &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/users/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/facts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                               &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;episode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/episodes/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;kwargs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;top_k&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TOP_K&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;namespace_path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;by_path&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;namespace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ns&lt;/span&gt;
            &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kind&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
                      &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retrieve_memories&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# one bad namespace is not fatal
&lt;/span&gt;            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uc02 recall failed for %s: %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Writing the event starts extraction, which is how a later session finds a sentence the earlier one said. It is not part of the invoke: the call returned in 17 seconds, the record arrived later.&lt;/p&gt;

&lt;p&gt;Retrieval runs before the model, because the records become part of the system prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;records&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;load_model&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;What you remember about this manager:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# ... tools=list(TOOLS) and conversation_manager=NullConversationManager() too
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A fresh &lt;code&gt;Agent&lt;/code&gt; per invoke: a cached one would keep serving the first turn's memories. A retrieval failure is logged and swallowed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy it
&lt;/h2&gt;

&lt;p&gt;From the use case folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore deploy &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--target&lt;/span&gt; default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8ycuwdbaycpqgyhbwoc7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8ycuwdbaycpqgyhbwoc7.png" alt="Terminal capture: the deploy steps and the two resource ids" width="800" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Wall clock: 4 minutes 20 seconds, ending in a memory id and a runtime ARN.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS2-status.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS2-status.png" alt="Terminal capture: one runtime READY and one memory with three strategies" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One contract, so one deploy creates both halves, and the memory names its three strategies: the cheapest proof the extraction was requested.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore invoke &lt;span class="nt"&gt;--user-id&lt;/span&gt; store_manager_south &lt;span class="nt"&gt;--session-id&lt;/span&gt; 7d1f4a0c9b2e4d38a5c6e7f801234567-229cdb2d &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--prompt&lt;/span&gt; &lt;span class="s2"&gt;"From now on, only reorder for the South China region. Ignore every other region."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS3-preference-south.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS3-preference-south.png" alt="Terminal capture: the preference is accepted and applied immediately" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A new session, four minutes later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore invoke &lt;span class="nt"&gt;--user-id&lt;/span&gt; store_manager_south &lt;span class="nt"&gt;--session-id&lt;/span&gt; 8b2c5d1e0f3a4b6c9d8e7f6054321098-33438667 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--prompt&lt;/span&gt; &lt;span class="s2"&gt;"Draft this week's reorder list."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS4-recall-south.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS4-recall-south.png" alt="Terminal capture: the new session reorders for South China without being told" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;255 seconds separate the event from the answer, and this session never saw the sentence. Only the memory resource could have supplied it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore invoke &lt;span class="nt"&gt;--user-id&lt;/span&gt; store_manager_east &lt;span class="nt"&gt;--session-id&lt;/span&gt; 9c3d6e2f1a4b5c7d8e9f0a1b23456789-584aa045 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--prompt&lt;/span&gt; &lt;span class="s2"&gt;"From now on, only reorder for the East China region. Ignore every other region."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS5-preference-east.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS5-preference-east.png" alt="Terminal capture: the eastern manager's first turn, before extraction has run" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore invoke &lt;span class="nt"&gt;--user-id&lt;/span&gt; store_manager_east &lt;span class="nt"&gt;--session-id&lt;/span&gt; 0d4e7f3a2b5c6d8e9f0a1b2c34567890-37ad719d &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--prompt&lt;/span&gt; &lt;span class="s2"&gt;"Draft this week's reorder list."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS6-recall-east.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS6-recall-east.png" alt="Terminal capture: East China, from a session that never mentioned a region" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;236 seconds after its own event, the eastern manager is answered for East China. Now the same question to the southern one, seconds later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore invoke &lt;span class="nt"&gt;--user-id&lt;/span&gt; store_manager_south &lt;span class="nt"&gt;--session-id&lt;/span&gt; 1e5f8a4b3c6d7e9f0a1b2c3d45678901-4a0d1bea &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--prompt&lt;/span&gt; &lt;span class="s2"&gt;"Draft this week's reorder list."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS7-isolation.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS7-isolation.png" alt="Terminal capture: the same prompt, the other manager, the other region" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two actors, one prompt, two answers, no overlap: the actor id is part of the key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore invoke &lt;span class="nt"&gt;--user-id&lt;/span&gt; store_manager_east &lt;span class="nt"&gt;--session-id&lt;/span&gt; 2f6a9b5c4d7e8f0a1b2c3d4e56789012-0fa33c38 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--prompt&lt;/span&gt; &lt;span class="s2"&gt;"I tried restocking my stores from the Guangzhou warehouse last month. It failed: the lead time was 9 days and three stores stocked out. Make a note of it."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS8-episode.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS8-episode.png" alt="Terminal capture: a failed restock noted in one session" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Seventeen minutes later, nobody re-sends that sentence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore invoke &lt;span class="nt"&gt;--user-id&lt;/span&gt; store_manager_east &lt;span class="nt"&gt;--session-id&lt;/span&gt; 3a7b0c6d5e8f9a1b2c3d4e5f67890123-31527d8a &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--prompt&lt;/span&gt; &lt;span class="s2"&gt;"Is it a good idea to ship this week's East China restock from the Guangzhou warehouse?"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS9-episode-recall.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS9-episode-recall.png" alt="Terminal capture: the answer quotes the 9-day lead time nobody mentioned" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;1,022 seconds after the episode was written, the answer recommends against Guangzhou and names the 9-day lead time: the episodic strategy recalling an event by meaning.&lt;/p&gt;

&lt;p&gt;Every invoke left a trace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore traces list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS10-traces-list.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS10-traces-list.png" alt="Terminal capture: one row per invoke of this runtime" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore traces get 6ad46781381df402758baa6d42b96dd3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS11-trace-get.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS11-trace-get.png" alt="Terminal capture: the trace is written to a local JSON file" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS12-trace-spans.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS12-trace-spans.png" alt="Terminal capture: the span table, three memory reads before one model call" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three &lt;code&gt;RetrieveMemoryRecords&lt;/code&gt; calls at 349, 268 and 262 milliseconds, one per namespace, then &lt;code&gt;invoke_agent&lt;/code&gt; at 2,228 milliseconds with 1,400 input and 94 output tokens. Memory work happens before the model runs, which is what the code promises.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws resourcegroupstaggingapi get-resources &lt;span class="nt"&gt;--tag-filters&lt;/span&gt; &lt;span class="nv"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Project,Values&lt;span class="o"&gt;=&lt;/span&gt;agentcore-in-action &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS13-independence.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS13-independence.png" alt="Terminal capture: four resources, every one tagged UseCase=02" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Four resources, all this use case.&lt;/p&gt;

&lt;p&gt;Teardown is two commands, and the second one deletes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore remove all &lt;span class="nt"&gt;-y&lt;/span&gt;
agentcore deploy &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--target&lt;/span&gt; default
agentcore status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS14-teardown.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fagentcore-memory-that-remembers%2FS14-teardown.png" alt="Terminal capture: remove all, the reconciling deploy, an empty account" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;remove all&lt;/code&gt; empties the local contract and deletes nothing on AWS; the deploy after it reconciles the stack to nothing in 4 minutes 13 seconds, and the tag query then returns zero rows. Both JSON files are emptied, so restore them from git.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it produced
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd5gzji22p05aotv9af3g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd5gzji22p05aotv9af3g.png" alt="The memory resource as two halves: an event log and three namespaces keyed by actor id" width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2v02ywrxerrq9j11yo4a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2v02ywrxerrq9j11yo4a.png" alt="The cross-session loop: an event, its extracted record, and a second session reading the record" width="799" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three actors, three namespaces, one prompt producing two different lists. Extraction lag was 255 seconds for a preference and 1,022 for an episode, which decides every deadline you write.&lt;/p&gt;

&lt;p&gt;A prefix read of &lt;code&gt;/episodes/store_manager_east/&lt;/code&gt; returns a &lt;code&gt;BASE&lt;/code&gt; episode filed under its session and a &lt;code&gt;REFLECTION&lt;/code&gt; written one level up, hence the &lt;code&gt;{sessionId}&lt;/code&gt; in one template and not the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Invalid length for parameter runtimeSessionId, value: 22, valid min length: 33&lt;/code&gt;. Short readable session ids fail before they are sent. Build them from two uuid4 halves, because one hex uuid is 32 characters, one short of the minimum.&lt;/li&gt;
&lt;li&gt;The actor id sent as &lt;code&gt;-H x-amzn-bedrock-agentcore-runtime-custom-actor-id: ...&lt;/code&gt; never reached the agent, and the reply described the northern manager's memory because the code fell back to its default actor. Send it as &lt;code&gt;--user-id&lt;/code&gt;, which arrives as &lt;code&gt;x-amzn-bedrock-agentcore-runtime-user-id&lt;/code&gt;, and log the header names you receive once. This cost one deploy to find and is kept in &lt;code&gt;evidence/27&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Episodic extraction took 17 minutes, so a 300-second deadline can only fail. Choose the deadline from a measurement.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;remove all&lt;/code&gt; needs &lt;code&gt;-y&lt;/code&gt; in any non-interactive shell, and it empties &lt;code&gt;agentcore.json&lt;/code&gt; and &lt;code&gt;aws-targets.json&lt;/code&gt;. Restore both from git before deploying again.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Memory is two mechanisms: an event log you write, and records the service extracts. Only the second reaches a new session.&lt;/li&gt;
&lt;li&gt;Put the actor id inside the namespace template. Isolation then costs nothing and cannot be forgotten in a code path.&lt;/li&gt;
&lt;li&gt;Extraction is asynchronous and slow, and strategies differ. Every assertion needs a deadline longer than the slowest one.&lt;/li&gt;
&lt;li&gt;Send the actor id with &lt;code&gt;--user-id&lt;/code&gt; and verify it arrived. A silent fallback to a default actor looks like working memory and is the opposite.&lt;/li&gt;
&lt;li&gt;Teardown is real: &lt;code&gt;remove all&lt;/code&gt; empties the contract, the deploy after it does the deleting.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>agentcore</category>
      <category>llm</category>
    </item>
    <item>
      <title>Your First Agent as a Service: Deploying a Support Agent on AgentCore</title>
      <dc:creator>Pin Xiong</dc:creator>
      <pubDate>Sun, 18 Oct 2026 02:43:35 +0000</pubDate>
      <link>https://dev.to/aws-builders/your-first-agent-as-a-service-deploying-a-support-agent-on-agentcore-48hh</link>
      <guid>https://dev.to/aws-builders/your-first-agent-as-a-service-deploying-a-support-agent-on-agentcore-48hh</guid>
      <description>&lt;p&gt;A support team wants its first automation. This article is the walkthrough: one folder, one contract, and the &lt;code&gt;agentcore&lt;/code&gt; and &lt;code&gt;aws&lt;/code&gt; commands that deploy it, prove it, and delete it again. Every command below was run while writing this, and every figure is that command's captured output.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fby9qr676raie7zeel7z1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fby9qr676raie7zeel7z1.png" alt="A support agent as a managed runtime: what you own, what AWS hosts, and the two things the runtime talks to" width="800" height="522"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The use case
&lt;/h2&gt;

&lt;p&gt;A three-person support desk answers the same questions all day: refund totals, return windows, policy lines. They want a first automation, and nobody on the team has ever put a service in production. The ask is not intelligence, it is a loop they can trust: write something small, make it callable from anywhere, see what it did afterwards, and be able to delete it the same day.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AgentCore does here
&lt;/h2&gt;

&lt;p&gt;Two of the nine AgentCore capabilities carry this use case.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runtime: hosts &lt;code&gt;app/aia_uc01/&lt;/code&gt; as a managed endpoint. The build is CodeZip, so nothing is containerized; each session gets its own microVM; the protocol is plain HTTP over &lt;code&gt;/ping&lt;/code&gt; and &lt;code&gt;/invocations&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Observability: every invoke emits OpenTelemetry spans into X-Ray, so the tool call is visible as its own span and not just as text in a log.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The other seven stay off. No Memory, so the agent keeps nothing between turns. No Gateway and no Identity, because the desk has no external systems to reach yet. The runtime is version 1 behind the DEFAULT endpoint; changing the JSON and redeploying produces version 2, which is the whole versioning story at this level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 20 or newer (22.22.2 here), the AgentCore CLI 0.31.1, AWS CLI 2.x, jq 1.7.1, uv 0.11.9&lt;/li&gt;
&lt;li&gt;Python 3.10 to 3.14 on the host; the runtime target is PYTHON_3_13, set in the contract&lt;/li&gt;
&lt;li&gt;An AWS account with Bedrock model access; region us-east-1 for everything&lt;/li&gt;
&lt;li&gt;Model: deepseek.v3.2, passed to the code through the &lt;code&gt;MODEL_ID&lt;/code&gt; environment variable&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/pinxiong/bedrock-agentcore-in-action/tree/master/usecases/01-first-agent" rel="noopener noreferrer"&gt;https://github.com/pinxiong/bedrock-agentcore-in-action/tree/master/usecases/01-first-agent&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;01-first-agent/
├── agentcore/
│   ├── agentcore.json         # the contract: what gets deployed
│   └── aws-targets.json       # account 123456789012, region us-east-1
├── app/aia_uc01/
│   ├── main.py                # the agent: prompt, tool, sanitizer, entrypoint
│   ├── model/load.py          # MODEL_ID -&amp;gt; BedrockModel
│   ├── model/sanitize.py      # strips chat-template scaffolding from the stream
│   └── pyproject.toml         # dependencies packaged into the zip
└── tests/
    └── test_sanitize.py       # six offline cases for the sanitizer, no AWS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two JSON files are the whole contract; &lt;code&gt;main.py&lt;/code&gt; is the whole agent. The folder ships no script of its own: every operation below is an &lt;code&gt;agentcore&lt;/code&gt; or &lt;code&gt;aws&lt;/code&gt; command.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The whole agent surface fits in one screen of &lt;code&gt;main.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;SYSTEM_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You are the first-line assistant for the Northwind Gear
support desk. Answer in one short sentence.
When a question involves arithmetic, call the add_numbers tool instead of
working it out yourself, then state the result in one short sentence.
You have no order database, no ticket history and no account lookup. When a
question needs one of those, say you cannot see it instead of guessing.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_numbers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return the sum of two numbers.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;

&lt;span class="nd"&gt;@app.entrypoint&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;session_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default-session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_or_create_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;scrub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StreamSanitizer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;extract_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# every text delta passes through scrub.feed() before it is yielded
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt is the product. The tool exists so the trace has something to show; an agent that never calls a tool produces a single span and proves little. The entrypoint is an async generator, which is why streaming is the default shape here rather than an add-on.&lt;/p&gt;

&lt;p&gt;The part worth copying is the filter around the stream. DeepSeek models sometimes write fragments of their own chat-template markers, such as &lt;code&gt;&amp;lt;|DSML|function_calls&lt;/code&gt;, into the ordinary text channel. The sanitizer holds back a short suffix that could still grow into a marker, resets once per assistant message, and flushes what is left when the message ends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messageStart&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;scrub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StreamSanitizer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;      &lt;span class="c1"&gt;# one clean filter per assistant message
&lt;/span&gt;    &lt;span class="n"&gt;block_index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;
    &lt;span class="k"&gt;continue&lt;/span&gt;
&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;clean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scrub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;# tool-use deltas pass through untouched
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;_text_delta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block_index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its six cases live in &lt;code&gt;tests/test_sanitize.py&lt;/code&gt; and run offline, no AWS involved, because a filter you cannot test cheaply is a filter you will delete.&lt;/p&gt;

&lt;p&gt;Model selection is one function, because the default matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_model&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;BedrockModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;BedrockModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MODEL_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DEFAULT_MODEL_ID&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;MODEL_ID&lt;/code&gt; is missing, an SDK default can pick a model your region refuses; passing the id explicitly is what keeps the first deploy from failing on geography.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy it
&lt;/h2&gt;

&lt;p&gt;From the use case folder, one command builds the zip, synthesizes the CloudFormation and creates the stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore deploy &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--target&lt;/span&gt; default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frt8mntmzvnvzxcw7zew8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frt8mntmzvnvzxcw7zew8.png" alt="Terminal capture: the deploy steps, the runtime id and the runtime ARN it printed" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The stack this contract produces is four resources: an AWS::BedrockAgentCore::Runtime, the AWS::IAM::Role it runs as, that role's AWS::IAM::Policy, and AWS::CDK::Metadata (recorded in &lt;code&gt;evidence/23-stack-resources.txt&lt;/code&gt;). The deploy log splits the total: 4.1 s synthesizing CloudFormation, 1 m 17 s creating the stack, 1 m 31 s end to end. Then status, and one real invoke:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fyour-first-agent-as-a-service-on-agentcore%2FS2-status.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fyour-first-agent-as-a-service-on-agentcore%2FS2-status.png" alt="Terminal capture: Runtime READY and the invoke URL the CLI hands back" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore invoke &lt;span class="nt"&gt;--prompt&lt;/span&gt; &lt;span class="s2"&gt;"A customer returns two items priced 21 each. What refund total should I quote? Use the tool."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fyour-first-agent-as-a-service-on-agentcore%2FS3-invoke.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fyour-first-agent-as-a-service-on-agentcore%2FS3-invoke.png" alt="Terminal capture: the deployed invoke answering the refund question with 42" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What the deploy produced is provable from the CLI alone; the four things an assertion script used to check are four things this output shows.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;agentcore status&lt;/code&gt; printed &lt;code&gt;Deployed - Runtime: READY&lt;/code&gt; and the runtime ARN, so the runtime exists in us-east-1 and can serve traffic.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;agentcore invoke&lt;/code&gt; answered a tool-using prompt with the right arithmetic and no model scaffolding in the reply.&lt;/li&gt;
&lt;li&gt;The invoke left a trace, listed below, whose span tree contains an &lt;code&gt;execute_tool&lt;/code&gt; span.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aws resourcegroupstaggingapi get-resources&lt;/code&gt; returned three rows, all tagged &lt;code&gt;UseCase=01&lt;/code&gt;: no other use case's resources are in the account.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore traces list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fyour-first-agent-as-a-service-on-agentcore%2FS4-traces-list.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fyour-first-agent-as-a-service-on-agentcore%2FS4-traces-list.png" alt="Terminal capture: the trace from that session, listed about three minutes after the invoke" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore traces get 6ad45817558974710d8c3b6d35f5a089
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fyour-first-agent-as-a-service-on-agentcore%2FS5-trace-get.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fyour-first-agent-as-a-service-on-agentcore%2FS5-trace-get.png" alt="Terminal capture: the trace downloaded to a JSON file under agentcore/.cli/traces" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'[.[] ."@message" | select(type == "object" and .name != null)] as $spans
  | ($spans | map({(.spanId): .name}) | add) as $parent
  | $spans | sort_by(.startTimeUnixNano) | .[] | (.attributes // {}) as $a
  | [.name, .kind, (((.durationNano // 0) / 1000000 | floor | tostring) + " ms"),
     (if $a["gen_ai.usage.input_tokens"] then ($a["gen_ai.usage.input_tokens"] | tostring) + " in / " + ($a["gen_ai.usage.output_tokens"] | tostring) + " out" else "-" end),
     ($parent[.parentSpanId // ""] // "root")] | @tsv'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  agentcore/.cli/traces/aia_uc01-6ad45817558974710d8c3b6d35f5a089.json | column &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fyour-first-agent-as-a-service-on-agentcore%2FS6-trace-spans.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fyour-first-agent-as-a-service-on-agentcore%2FS6-trace-spans.png" alt="Terminal capture: nine spans out of the downloaded trace, including execute_tool add_numbers" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws resourcegroupstaggingapi get-resources &lt;span class="nt"&gt;--tag-filters&lt;/span&gt; &lt;span class="nv"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Project,Values&lt;span class="o"&gt;=&lt;/span&gt;agentcore-in-action &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'ResourceTagMappingList[].{arn:ResourceARN,tags:Tags[?Key==`UseCase`].Value|[0]}'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fyour-first-agent-as-a-service-on-agentcore%2FS7-independence.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fyour-first-agent-as-a-service-on-agentcore%2FS7-independence.png" alt="Terminal capture: three tagged resources, every one of them UseCase=01" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Teardown is two commands, and the second one does the deleting. &lt;code&gt;agentcore remove all&lt;/code&gt; empties the local contract; the deploy that follows reconciles the stack to nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore remove all &lt;span class="nt"&gt;-y&lt;/span&gt;
agentcore deploy &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--target&lt;/span&gt; default
agentcore status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fyour-first-agent-as-a-service-on-agentcore%2FS8-teardown.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fxiongpin.dev%2Fimages%2Fyour-first-agent-as-a-service-on-agentcore%2FS8-teardown.png" alt="Terminal capture: the schema reset, the stack teardown, and status finding nothing left" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F13frftdgvcjg54g7op8g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F13frftdgvcjg54g7op8g.png" alt="The six commands of the loop and what each one cost on this run" width="799" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Six commands, and the loop closes where it started: nothing deployed, nothing tagged, one trace kept.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it produced
&lt;/h2&gt;

&lt;p&gt;The invoke is unremarkable on purpose: one sentence, the right arithmetic, nothing else. The interesting artifact is the span tree, and it reads like the agent loop drawn by the runtime itself:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcloazvy1mp3lwwcclnwl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcloazvy1mp3lwwcclnwl.png" alt="The span tree drawn from the X-Ray trace: one span per step of the loop" width="800" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;execute_tool&lt;/code&gt; span is the point. It is what separates "the model said 42" from "the tool computed 42 and the model reported it", and it only exists on the deployed runtime; traces from a local &lt;code&gt;agentcore dev&lt;/code&gt; session record the tool call in logs but not as a span.&lt;/p&gt;

&lt;p&gt;Cost, measured on this run: the traced invoke used 955 input and 81 output tokens, which at deepseek.v3.2 prices in us-east-1, 0.62 and 1.85 USD per million tokens, is 0.00074 USD of model calls. The runtime bills per use, so an idle deployed agent costs nothing, and the teardown leaves nothing tagged behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;agentcore remove all&lt;/code&gt; without &lt;code&gt;-y&lt;/code&gt; dies in a non-interactive shell with "This command requires an interactive terminal". Any piped or CI run hits this. The fix is the &lt;code&gt;-y&lt;/code&gt; flag, and it is not optional in a script.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;agentcore remove all -y&lt;/code&gt; resets every schema in &lt;code&gt;agentcore.json&lt;/code&gt; and empties &lt;code&gt;aws-targets.json&lt;/code&gt;, so the next deploy has no target and &lt;code&gt;agentcore status&lt;/code&gt; answers &lt;code&gt;No resources match the given filters&lt;/code&gt;. Restore both files from git before you deploy again.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The contract is two JSON files; everything else on the AWS side is generated, so the deploy is reproducible and the teardown is real.&lt;/li&gt;
&lt;li&gt;CodeZip means no Dockerfile: the platform zips &lt;code&gt;codeLocation&lt;/code&gt;, and the runtime version and endpoint come from the same contract.&lt;/li&gt;
&lt;li&gt;An agent is not observable until a tool call has its own span; &lt;code&gt;agentcore traces get&lt;/code&gt; plus &lt;code&gt;jq&lt;/code&gt; is the whole proof, no test harness required.&lt;/li&gt;
&lt;li&gt;A stream sanitizer belongs in the entrypoint from day one, because scaffolding leaks are intermittent and a test-only filter rots.&lt;/li&gt;
&lt;li&gt;The full loop, measured: deploy 1 m 31 s, invoke 9 954 ms, teardown 1 m 40 s, and &lt;code&gt;remove all -y&lt;/code&gt; to &lt;code&gt;deploy -y&lt;/code&gt; in that order.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>agentcore</category>
      <category>aws</category>
    </item>
    <item>
      <title>Mastering Amazon EC2: Compute, Storage, Networking and Recovery</title>
      <dc:creator>Venkata Pavan Vishnu Rachapudi</dc:creator>
      <pubDate>Sat, 17 Oct 2026 16:41:22 +0000</pubDate>
      <link>https://dev.to/aws-builders/mastering-amazon-ec2-compute-storage-networking-and-recovery-31df</link>
      <guid>https://dev.to/aws-builders/mastering-amazon-ec2-compute-storage-networking-and-recovery-31df</guid>
      <description>&lt;p&gt;&lt;strong&gt;Amazon&lt;/strong&gt; EC2 is still one of the most important building blocks of AWS. But working with EC2 is about much more than launching an instance.&lt;/p&gt;

&lt;p&gt;I'm excited to share my book, &lt;strong&gt;Mastering Amazon EC2: Compute, Storage, Networking and Recovery&lt;/strong&gt;, a practical guide to how EC2 works in real-world environments.&lt;/p&gt;

&lt;p&gt;It covers &lt;strong&gt;EC2 fundamentals, secure access, IAM, VPC networking, EBS, AMIs, load balancing, Auto Scaling, monitoring with CloudWatch and Amazon Q, backup with AWS Backup, migration with AWS Transform MGN, and disaster recovery&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every chapter ends with a hands-on lab in your own AWS account, and field stories from production work show what happens when things break.&lt;/p&gt;

&lt;p&gt;It's written for &lt;strong&gt;cloud engineers, DevOps engineers, platform engineers, AWS learners, and anyone preparing for AWS certifications&lt;/strong&gt;, in India and across the global AWS community.&lt;/p&gt;

&lt;p&gt;The goal is simple: help you move from &lt;strong&gt;launching EC2 instances to securing, scaling, monitoring, and recovering production workloads&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;📘 &lt;strong&gt;Available on Amazon:&lt;/strong&gt;&lt;br&gt;
🇮🇳 India: &lt;a href="https://www.amazon.in/dp/B0HN5TCVML" rel="noopener noreferrer"&gt;https://www.amazon.in/dp/B0HN5TCVML&lt;/a&gt;&lt;br&gt;
🌍 Global: &lt;a href="https://www.amazon.com/dp/B0HN5TCVML" rel="noopener noreferrer"&gt;https://www.amazon.com/dp/B0HN5TCVML&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Would love you're views &lt;/p&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>books</category>
      <category>beginners</category>
    </item>
    <item>
      <title>What does your CDK app cost when nobody is using it?</title>
      <dc:creator>kazuho cryer-shinozuka</dc:creator>
      <pubDate>Sat, 17 Oct 2026 13:44:59 +0000</pubDate>
      <link>https://dev.to/aws-builders/what-does-your-cdk-app-cost-when-nobody-is-using-it-jp4</link>
      <guid>https://dev.to/aws-builders/what-does-your-cdk-app-cost-when-nobody-is-using-it-jp4</guid>
      <description>&lt;p&gt;Here is a sample CDK app with four stacks. Nobody is calling it. No requests, no data, no users.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ap-northeast-1 · 4 stacks · idle ≈ $382/mo

DataStack                                    $196/mo
  Aurora/writer                 ×0.5 ACU      $55  Serverless v2 minimum capacity
  Search                        ×1            $41  t3.small.search data nodes
  Search                        ×20 GB gp3  $2.93  EBS storage
  Db                            ×1            $36  db.t4g.micro postgres Multi-AZ
  Db                            ×20 GB gp3  $5.52  storage
  ...

NetworkStack                                  $69/mo
  Vpc/PublicSubnet1/NATGateway  ×1            $45  NAT Gateway
  Vpc/SsmEndpoint               ×2 AZ         $20  interface endpoint
  Vpc/PublicSubnet1/EIP         ×1          $3.65  public IPv4 address

? could not estimate: 1
  AppStack  ParamInstance  InstanceType is Parameter WorkerType

prices: bundled snapshot 2026-10-15 · on-demand list prices, 730 h/mo, USD, free tier ignored
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It still costs about &lt;strong&gt;$382 a month&lt;/strong&gt;. That output came from two commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cdk synth
npx cfn-idle-cost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/badmintoncryer/cfn-idle-cost" rel="noopener noreferrer"&gt;cfn-idle-cost&lt;/a&gt; reads your &lt;code&gt;cdk.out&lt;/code&gt; and prints the &lt;strong&gt;idle cost&lt;/strong&gt; of the app: what you pay even when nobody uses it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why idle cost?
&lt;/h2&gt;

&lt;p&gt;Most surprise AWS bills I have seen were not caused by traffic. They were caused by things that bill by the hour whether or not anyone uses them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a NAT Gateway in every AZ of a dev VPC&lt;/li&gt;
&lt;li&gt;interface endpoints, billed per AZ&lt;/li&gt;
&lt;li&gt;an ALB in front of a service that gets ten requests a day&lt;/li&gt;
&lt;li&gt;an RDS instance someone set to Multi-AZ "to be safe"&lt;/li&gt;
&lt;li&gt;an Aurora Serverless v2 cluster that never scales below its minimum ACU&lt;/li&gt;
&lt;li&gt;KMS keys, Secrets Manager secrets and CloudWatch alarms that add up one dollar at a time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In CDK, a single &lt;code&gt;new ec2.Vpc(this, 'Vpc')&lt;/code&gt; creates NAT Gateways for you. That default is reasonable for production, but it is easy to miss when you copy the same stack into a sandbox account or a per-developer environment.&lt;/p&gt;

&lt;p&gt;Cost Explorer tells you about this after the bill arrives. The AWS Pricing Calculator can tell you before, but you have to enter every resource by hand. cfn-idle-cost reads the template you already have.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reads &lt;code&gt;cdk.out&lt;/code&gt; directly&lt;/strong&gt;, including Stages and nested stacks. Plain CloudFormation templates (JSON or YAML) work too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Names each line by construct path&lt;/strong&gt; (&lt;code&gt;Vpc/PublicSubnet1/NATGateway&lt;/code&gt;), so you know which line of CDK code to change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runs offline with no AWS credentials.&lt;/strong&gt; Prices come from a snapshot of the public AWS Bulk Price List bundled in the package and refreshed weekly. The same &lt;code&gt;cdk.out&lt;/code&gt; gives the same numbers for a given version. Pass &lt;code&gt;--live&lt;/code&gt; if you want current prices from the Price List Query API instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never guesses.&lt;/strong&gt; If a value can't be read from the template, such as a Parameter without a default or a reference to another stack, the resource is listed under &lt;code&gt;? could not estimate&lt;/code&gt; with the reason. A Parameter with a &lt;code&gt;Default&lt;/code&gt; is estimated with that default, and the line says so.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It covers more than 70 resource types: NAT Gateways, EIPs, VPC endpoints, load balancers, EC2 and Auto Scaling groups, RDS and Aurora, OpenSearch, ElastiCache, DynamoDB provisioned capacity, Kinesis, Fargate services, EFS and FSx, EKS, Network Firewall, WAF, Lambda provisioned concurrency, Transit Gateway attachments, MWAA, and more. The &lt;a href="https://github.com/badmintoncryer/cfn-idle-cost#what-is-counted" rel="noopener noreferrer"&gt;full list&lt;/a&gt; is in the README.&lt;/p&gt;

&lt;p&gt;Usage-only charges are left out on purpose: S3, Lambda invocations, Aurora storage and I/O, CloudWatch Logs, data transfer, requests. Those depend on traffic, and this tool answers a different question: what is the floor?&lt;/p&gt;

&lt;h2&gt;
  
  
  Catch it in the pull request
&lt;/h2&gt;

&lt;p&gt;Knowing the current idle cost is useful. Knowing that a pull request is about to add $300 a month is more useful.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--base&lt;/code&gt; compares two synthesized apps and prints only what changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx cfn-idle-cost &lt;span class="nt"&gt;--base&lt;/span&gt; cdk.out.main cdk.out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ap-northeast-1 · idle ≈ $382 → $682/mo (+$300)

  ~ DataStack     Db                              $42 →  $322   +$280  db.t4g.micro postgres Multi-AZ → db.t4g.large postgres Multi-AZ
  + NetworkStack  Vpc/PublicSubnet2/NATGateway  $0.00 →   $45    +$45  NAT Gateway
  - DataStack     Stream                          $28 → $0.00    -$28  provisioned shards
  + NetworkStack  Vpc/PublicSubnet2/EIP         $0.00 → $3.65  +$3.65  public IPv4 address
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a GitHub Action that posts this as a pull request comment, updates it on every push, and fails the check when the increase is over your budget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;idle-cost&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull_request&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;read&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;write&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;idle-cost&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;head&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;base&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;github.base_ref&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;22&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci &amp;amp;&amp;amp; npx cdk synth -q&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;head&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci &amp;amp;&amp;amp; npx cdk synth -q&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;base&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;badmintoncryer/cfn-idle-cost@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;base&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;base/cdk.out&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;head/cdk.out&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;max-increase&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;50&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the reviewer sees "+$280/mo: db.t4g.micro → db.t4g.large" next to the diff, instead of finding out from the bill a month later. Commit &lt;code&gt;cdk.context.json&lt;/code&gt; so the synth doesn't need AWS credentials for lookups.&lt;/p&gt;

&lt;p&gt;Outside GitHub Actions, &lt;code&gt;--max &amp;lt;usd&amp;gt;&lt;/code&gt; and &lt;code&gt;--max-increase &amp;lt;usd&amp;gt;&lt;/code&gt; make the CLI exit with code &lt;code&gt;3&lt;/code&gt; when the budget is exceeded, so it fits into any CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the numbers mean
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;On-demand list prices in USD, 730 hours a month.&lt;/li&gt;
&lt;li&gt;Savings Plans, Reserved Instances and the free tier are ignored.&lt;/li&gt;
&lt;li&gt;EC2 is priced as Linux with shared tenancy, because the AMI is not read.&lt;/li&gt;
&lt;li&gt;An Auto Scaling group uses &lt;code&gt;DesiredCapacity&lt;/code&gt; when set, otherwise &lt;code&gt;MinSize&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, it is the price list applied to your template, not a forecast of your bill. If you have a Savings Plan, your real number will be lower. That is fine: the point is to see what you are paying for and which construct it comes from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cdk synth
npx cfn-idle-cost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also install it with Go (&lt;code&gt;go install github.com/badmintoncryer/cfn-idle-cost@latest&lt;/code&gt;), and &lt;code&gt;cdk-idle-cost&lt;/code&gt; is the same tool under another name. The npm packages cover Linux and macOS; on Windows, use &lt;code&gt;go install&lt;/code&gt; or the binary from the GitHub Release.&lt;/p&gt;

&lt;p&gt;I'd love to hear what number your app prints. If it lists a resource under &lt;code&gt;! not counted yet&lt;/code&gt; that matters to you, or if a price looks wrong, please &lt;a href="https://github.com/badmintoncryer/cfn-idle-cost/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt;. And if it saves you a NAT Gateway or two, a ⭐ on &lt;a href="https://github.com/badmintoncryer/cfn-idle-cost" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps others find it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I also maintain two other tools that read &lt;code&gt;cdk.out&lt;/code&gt; before you deploy: &lt;a href="https://github.com/badmintoncryer/cdk-preflight" rel="noopener noreferrer"&gt;cdk-preflight&lt;/a&gt; fails &lt;code&gt;cdk synth&lt;/code&gt; on templates that are known to fail at deploy time, and &lt;a href="https://github.com/badmintoncryer/cfn-exec-policy" rel="noopener noreferrer"&gt;cfn-exec-policy&lt;/a&gt; generates a least-privilege IAM policy for the CloudFormation execution role, so &lt;code&gt;cdk bootstrap&lt;/code&gt; doesn't have to use &lt;code&gt;AdministratorAccess&lt;/code&gt;. More on those in later posts.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cdk</category>
      <category>finops</category>
      <category>devops</category>
    </item>
    <item>
      <title>Una novedad en Serverless bajo la lupa: AWS Lambda Web Functions</title>
      <dc:creator>Laura Bolaños</dc:creator>
      <pubDate>Fri, 16 Oct 2026 19:52:02 +0000</pubDate>
      <link>https://dev.to/aws-builders/una-novedad-en-serverless-bajo-la-lupa-aws-lambda-web-functions-nm4</link>
      <guid>https://dev.to/aws-builders/una-novedad-en-serverless-bajo-la-lupa-aws-lambda-web-functions-nm4</guid>
      <description>&lt;h2&gt;
  
  
  Introducción
&lt;/h2&gt;

&lt;p&gt;AWS anunció Lambda Web Functions, una forma de ejecutar aplicaciones web y APIs sobre Lambda ejecutando directamente un servidor HTTP de Node.js, sin adaptadores ni handlers por evento. Aquí te describo mi propio análisis.&lt;br&gt;
Este artículo no pretende ser una guía de cómo usar Lambda Web Functions. Hago un recorrido corto por las características que importan, referencio blogs de colegas y muestro mi propio enfoque de cuándo utilizarla y cuando No.&lt;br&gt;
Para no quedarme en la teoría, armé un pequeño playground con ejemplos: desde un agente de IA con streaming hasta microservicios simulando alto tráfico. Además, hice algunos experimentos de concurrencia y comportamiento de escalado.&lt;/p&gt;
&lt;h2&gt;
  
  
  Qué es una Lambda Web Function
&lt;/h2&gt;

&lt;p&gt;Una Lambda Web Function es cómputo serverless que corre un &lt;strong&gt;servidor HTTP normal&lt;/strong&gt;. Tu aplicación escucha en el host y puerto que Lambda te pasa en la variable &lt;code&gt;AWS_LAMBDA_HTTP_ENDPOINT&lt;/code&gt;, exactamente el mismo código que correrías con &lt;code&gt;node index.js&lt;/code&gt; en tu máquina. AWS te da el endpoint HTTPS, el TLS y el escalado; de tu parte toca escribir Express (o el framework que uses) sin adaptadores.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NO&lt;/strong&gt; es un reemplazo universal de la Lambda de siempre que devuelve un event handler. Esta sigue siendo la opción natural para lo que es dirigido por eventos: procesar mensajes de una SQS, reaccionar a un evento de S3, consumir un stream. Tampoco es un servidor que tengas "siempre encendido": escala a cero cuando no hay tráfico. Y por ahora solo soporta runtime &lt;strong&gt;Node.js&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Lo que cambia de verdad
&lt;/h3&gt;

&lt;p&gt;Muchos de los protocolos que vas a ver en las demos (REST, SSE, streaming, webhooks) ya se podían hacer antes de otra forma. Lo que cambia no son los protocolos, sino la &lt;strong&gt;forma&lt;/strong&gt; de la Lambda:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Modelo de programación.&lt;/strong&gt; Antes escribías un handler que recibía un evento; ahora escribís un servidor HTTP común que escucha en un puerto.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrencia.&lt;/strong&gt; En una Event Function, cada entorno atiende una petición a la vez. En una Web Function, un mismo entorno atiende varias en simultáneo, como un servidor Node común. Para algo que pasa casi todo el tiempo esperando (una base de datos, una API, un LLM), eso significa que muchas peticiones comparten un proceso en lugar de ocupar un entorno cada una.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parámetros importantes&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parámetro&lt;/th&gt;
&lt;th&gt;descripción&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxConcurrencyPerEnvironment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;tope por entorno, 1–128, se configura en la &lt;strong&gt;revisión&lt;/strong&gt;. El límite real puede ser menor, nunca mayor. Cambiarlo crea una nueva revisión&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxEnvironments&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;tope de entornos por región, se configura en el &lt;strong&gt;endpoint&lt;/strong&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rateLimit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;peticiones por segundo que acepta el &lt;strong&gt;endpoint&lt;/strong&gt;, por región. Lo que supera rateLimit recibe 429. Lo mismo ocurre si no puede escalar más, pero mientras un entorno nuevo se inicializa, las requests esperan en cola. rateLimit se aplica por región, y sus valores deben ser múltiplos de 100 (hasta 1.000) o de 1.000 (por encima).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;maxEnvironments&lt;/code&gt; y &lt;code&gt;rateLimit&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;se cambian en caliente con &lt;code&gt;aws lambda-web update-web-function-endpoint&lt;/code&gt;, sin redeployar; en cambio &lt;code&gt;maxConcurrencyPerEnvironment&lt;/code&gt; sí obliga a hacer una nueva revisión de la Lambda.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Entrada y salida: HTTP crudo, no un payload.&lt;/strong&gt; Una Event Function recibe un &lt;code&gt;event&lt;/code&gt; (un JSON ya parseado) y devuelve otro JSON; nunca tocás HTTP a mano. Una Web Function recibe y devuelve &lt;strong&gt;HTTP/1.1 crudo&lt;/strong&gt;: Lambda reenvía a tu app el método, el path, la query string, el body y casi todos los headers, y tu servidor (Express, Fastify, Hono o el framework de Node que prefieras) ve un &lt;code&gt;Request&lt;/code&gt;/&lt;code&gt;Response&lt;/code&gt; normal. Soporta GET, HEAD, POST, PUT, PATCH, OPTIONS y DELETE, solo HTTP/1.1 (un HTTP/1.0 lo rechaza con 505), y agrega headers de contexto como &lt;code&gt;lambda-web-request-id&lt;/code&gt;. Es justo lo que te deja usar tu framework sin adaptador, pero el parseo y la validación del request quedan de tu lado.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seguridad: el endpoint es público, el modo de auth define quién valida.&lt;/strong&gt; Todo endpoint es alcanzable desde internet, tenga el auth que tenga. Hay dos modos: con &lt;code&gt;IamAuth&lt;/code&gt;, Lambda valida la firma SigV4 y remueve el header &lt;code&gt;Authorization&lt;/code&gt; antes de reenviar (la autorización la hace la plataforma); con &lt;code&gt;ApplicationManaged&lt;/code&gt;, Lambda no auténtica nada y reenvía todo a tu app, así que conocer la URL alcanza para llegar y es necesario implementar la autenticación. Sumá a eso que algunos headers no son de fiar (&lt;code&gt;X-Forwarded-Host&lt;/code&gt; llega sin validar; el cliente real está en el último valor de &lt;code&gt;X-Forwarded-For&lt;/code&gt;) y que validar input, tamaño de request y abuso es responsabilidad de tu código o de un front door.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Facturación:&lt;/strong&gt; Pagás &lt;strong&gt;CPU activa&lt;/strong&gt; y &lt;strong&gt;memoria&lt;/strong&gt; mientras el entorno exista. No se cobra tiempo de reloj. Una petición que espera a una base de datos o a una API externa consume poca CPU, y esa espera casi no se cobra.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proceso de larga vida.&lt;/strong&gt; El entorno no se congela entre peticiones → hay estado global, conexiones reutilizadas y tareas en segundo plano.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⭐ Para el detalle de cada protocolo, el recorrido de &lt;a href="https://edjgeek.com/blog/why-i-m-excited-about-lambda-web-functions/" rel="noopener noreferrer"&gt;edjgeek&lt;/a&gt; lo cubre muy bien, y el repositorio de ejemplos que acompaña ese blog muestra REST, SSE, NDJSON y webhooks funcionando.&lt;/p&gt;
&lt;h3&gt;
  
  
  Dónde NO lo usaría
&lt;/h3&gt;

&lt;p&gt;Hay varios escenarios donde esta tipo de Lambda no aplica:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Request/response simple y de bajo volumen.&lt;/strong&gt; Un formulario que dispara un mail, o una Lambda que devuelve un JSON corto cada tanto. Ahí una Event Function con su function URL es más simple y suficiente; migrar no te da ningún beneficio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trabajo dirigido por eventos.&lt;/strong&gt; Procesar una cola SQS, reaccionar a un archivo en S3, consumir un stream. Eso es territorio de las Event Functions, y la propia documentación lo recomienda así.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tareas largas.&lt;/strong&gt; El timeout de una petición llega hasta 900 s (15 min). Si tu proceso puede tardar más, no entra en una sola petición: hay que partirlo o usar otro mecanismo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSockets o HTTP/2.&lt;/strong&gt; no  mantiene conexiones bidireccionales persistentes de tipo TCP Upgrade abiertas directamente hacia el entorno de ejecución de la Lambda de forma continua. Si este es tu caso no se resuelve por aquí.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;En Tareas multi-paso elegimos Lambda durable functions.&lt;/strong&gt; Pensadas para flujos de trabajo resilientes de varios pasos que pueden durar hasta un año, con checkpoints, reintentos y estados de espera que no consumen cómputo mientras aguardan. Casos de uso: procesar pagos, orquestar pedidos, flujos con aprobación humana o workflows de IA de varios pasos.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⭐ La regla práctica: si tu carga no es "un servidor HTTP que atiende tráfico concurrente", probablemente Lambda Web Functions no corresponda a tu caso de uso.&lt;/p&gt;
&lt;h2&gt;
  
  
  ¿Reemplaza a API Gateway / function URLs?
&lt;/h2&gt;

&lt;p&gt;Una Web Function ya trae su propio endpoint HTTPS. Pero reemplaza en los casos simples, no es un reemplazo directo.&lt;/p&gt;

&lt;p&gt;Pensemos primero en las &lt;strong&gt;function URLs&lt;/strong&gt;, la URL pública de una Lambda común. Desde el lado del cliente, para un request/response sencillo, casi no hay diferencia: hacés una petición HTTP y recibís una respuesta. Si lo que tenés es una Lambda con su URL devolviendo un JSON corto, migrar a Web Functions no te cambia nada visible. Donde empieza a tener sentido es cuando aparece concurrencia con mucha espera, streaming, o una app con framework y varias rutas.&lt;/p&gt;

&lt;p&gt;Con &lt;strong&gt;API Gateway&lt;/strong&gt; la cuestión es diferente, porque son de distinta naturaleza: una Web Function es cómputo con un endpoint incluido, y API Gateway es una puerta de entrada que se pone delante del cómputo.&lt;/p&gt;

&lt;p&gt;Si tenés una app con su propio router (Express, Hono, Fastify), antes necesitabas API Gateway para exponerla; ahora el endpoint ya viene con HTTPS, autenticación y un límite global de peticiones. Para APIs internas o servicios entre equipos, eso suele alcanzar.&lt;/p&gt;

&lt;p&gt;Pero API Gateway sigue teniendo sentido como &lt;em&gt;front door&lt;/em&gt;. La propia documentación de buenas prácticas recomienda poner &lt;strong&gt;CloudFront o API Gateway delante&lt;/strong&gt; del endpoint cuando necesitás dominio propio, inspección con WAF, caché o controles centralizados en el borde. Y hay features que API Gateway te da y la Web Function no: planes de uso y API keys por cliente, authorizers (JWT, Cognito), cuotas por consumidor. El &lt;strong&gt;límite de peticiones de la Web Function es un tope por endpoint y región, no por cliente&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;⭐ Para una app o microservicio con su propio router, Lambda Web Functions te AHORRA una capa. Para una API pública con múltiples consumidores, cuotas y políticas en el borde, seguís necesitando API Gateway o CloudFront según el caso.&lt;/p&gt;
&lt;h2&gt;
  
  
  Casos de Uso del playground
&lt;/h2&gt;

&lt;p&gt;Construí ejemplos sencillos que pueden servirte para llevar a la práctica rápidamente tus casos de uso.&lt;/p&gt;

&lt;p&gt;📦 Aquí vas encontrar ejemplos con dos maneras de desplegar: manualmente por la CLi o via SAM : &lt;a href="https://github.com/reinalau/lambda-web-functions-playground" rel="noopener noreferrer"&gt;github.com/reinalau/lambda-web-functions-playground&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Lambda Web Functions ya es compatible con IaC:&lt;br&gt;
&lt;strong&gt;AWS CloudFormation&lt;/strong&gt; (&amp;nbsp;"AWS::Lambda::WebFunction&amp;nbsp; resource").&lt;br&gt;
&lt;strong&gt;AWS CDK&lt;/strong&gt; (L2 constructs available)&lt;br&gt;
&lt;strong&gt;AWS SAM&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Un agente strands harness con streaming
&lt;/h3&gt;

&lt;p&gt;Es un agente de IA hecho con strands harness, que responde preguntas sobre el clima y ayuda a planificar viajes. Recibe un mensaje, llama a herramientas que consultan una API pública (Open-Meteo), razona sobre los datos y devuelve la respuesta al navegador &lt;strong&gt;token a token&lt;/strong&gt; por SSE. Mantiene la conversación entre turnos, recuerda lo que ya le dijiste.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;%%{init: {"theme": "base", "themeVariables": {"fontSize": "30px"}}}%%
flowchart LR
    Client["Client / browser"] --&amp;gt;|"POST /chat"| Agent["weather-agent&amp;lt;br /&amp;gt;Lambda Web Function (Express)&amp;lt;br /&amp;gt;Strands harness"]
    Agent --&amp;gt;|"SSE: stream tokens"| Client
    Agent --&amp;gt;|"InvokeModelWithResponseStream"| Bedrock["Amazon Bedrock&amp;lt;br /&amp;gt;Nova 2 Lite"]
    Agent --&amp;gt;|"tools (no API key)"| OpenMeteo["Open-Meteo API&amp;lt;br /&amp;gt;geocode + forecast"]
    Agent --&amp;gt;|"SessionManager&amp;lt;br /&amp;gt;by conversationId"| S3[("Amazon S3&amp;lt;br /&amp;gt;session state")]

    style Client fill:#2563eb,stroke:#1e40af,color:#ffffff
    style Agent fill:#f59e0b,stroke:#b45309,color:#ffffff
    style Bedrock fill:#15803d,stroke:#0f5027,color:#ffffff
    style OpenMeteo fill:#64748b,stroke:#475569,color:#ffffff
    style S3 fill:#4ade80,stroke:#16a34a,color:#063d1e&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;strong&gt;Por qué con Web Functions y no con otra cosa →&lt;/strong&gt; Este caso pasa casi todo el tiempo &lt;em&gt;esperando&lt;/em&gt; al modelo y a las herramientas, consumiendo muy poca CPU, y esa espera casi no se cobra. Además:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;El modelo de servidor HTTP me deja usar el harness de Strands y Express tal cual, sin reescribir nada como handler ni armar un adaptador.&lt;/li&gt;
&lt;li&gt;El streaming de tokens por SSE es nativo: devuelvo la respuesta a medida que se genera, sin esperar a tenerla completa.&lt;/li&gt;
&lt;li&gt;La concurrencia por entorno significa que muchas conversaciones conviven en el mismo proceso mientras esperan al LLM, en vez de ocupar un entorno entero cada una (como pasaría con una Event Function).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En una Event Function con function URL pagaría el tiempo de reloj completo de cada conversación y ocuparía un entorno por request. Acá no.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La implementación:&lt;/strong&gt; Es Express corriendo como Web Function. El agente usa el &lt;strong&gt;harness&lt;/strong&gt; de Strands (TypeScript) con &lt;code&gt;builtinTools: []&lt;/code&gt; para dejar solo las dos herramientas de clima (nada de shell ni acceso a archivos: menor superficie), sobre un modelo &lt;strong&gt;Amazon Nova&lt;/strong&gt; en Bedrock.&lt;/p&gt;

&lt;p&gt;El punto fino es el estado: como el entorno es efímero y no hay afinidad de sesión, la conversación &lt;strong&gt;no puede vivir en memoria&lt;/strong&gt;; se persiste fuera, en S3, usando el &lt;code&gt;SessionManager&lt;/code&gt; del SDK. Cada conversación se identifica por un id, y cualquier entorno puede retomarla. El detalle completo lo encontrás en el README del ejemplo.&lt;/p&gt;

&lt;h3&gt;
  
  
  Un BFF con microservicios (pedidos + inventario)
&lt;/h3&gt;

&lt;p&gt;Este ejemplo consta de dos Lambda Web Functions que modelan un patrón BFF + microservicios, sin IA: solo API y DynamoDB. Un servicio &lt;code&gt;orders&lt;/code&gt; (el BFF, público) recibe un pedido, reserva stock llamando a un servicio &lt;code&gt;inventory&lt;/code&gt; (privado) y persiste el pedido. &lt;code&gt;inventory&lt;/code&gt; expone el stock y lo reserva con un &lt;code&gt;UpdateItem&lt;/code&gt; condicional que nunca lo deja en negativo (devuelve &lt;code&gt;409&lt;/code&gt; si no alcanza).&lt;br&gt;&lt;br&gt;
Este caso es de un "backend de alto tráfico que espera mucho I/O": cada pedido llama a otro servicio y a una base de datos, y pasa la mayor parte del tiempo esperando esas dos cosas.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;%%{init: {"theme": "base", "themeVariables": {"fontSize": "28px"}}}%%
flowchart LR
    Client["Client"] --&amp;gt;|"POST /orders (no signing)"| Orders["orders BFF&amp;lt;br /&amp;gt;Lambda Web Function&amp;lt;br /&amp;gt;ApplicationManaged"]
    Orders --&amp;gt;|"SigV4-signed call&amp;lt;br /&amp;gt;service: lambda&amp;lt;br /&amp;gt;x-amz-content-sha256"| Inventory["inventory&amp;lt;br /&amp;gt;Lambda Web Function&amp;lt;br /&amp;gt;IamAuth (private)"]
    Orders --&amp;gt;|"PutItem"| OrdersDB[("DynamoDB&amp;lt;br /&amp;gt;orders table")]
    Inventory --&amp;gt;|"conditional UpdateItem&amp;lt;br /&amp;gt;reserve stock or 409"| InventoryDB[("DynamoDB&amp;lt;br /&amp;gt;inventory table")]

    style Client fill:#22c55e,stroke:#15803d,color:#ffffff
    style Orders fill:#f59e0b,stroke:#b45309,color:#ffffff
    style Inventory fill:#f59e0b,stroke:#b45309,color:#ffffff
    style OrdersDB fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style InventoryDB fill:#3b82f6,stroke:#1d4ed8,color:#ffffff&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Algunas características:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;El servidor HTTP deja usar Express con su router tal cual; &lt;code&gt;orders&lt;/code&gt; enruta por dentro y no necesito una función por ruta ni API Gateway delante.&lt;/li&gt;
&lt;li&gt;La concurrencia por entorno deja que muchas peticiones convivan en el mismo proceso mientras esperan a &lt;code&gt;inventory&lt;/code&gt; y a DynamoDB.&lt;/li&gt;
&lt;li&gt;Cada servicio escala por separado: un pico en el BFF no obliga a sobredimensionar el inventario.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Comunicación entre servicios con IamAuth.&lt;/strong&gt; La mayoría de las reseñas lo pasa por alto: &lt;code&gt;inventory&lt;/code&gt; es privado (&lt;code&gt;IamAuth&lt;/code&gt;), así que &lt;code&gt;orders&lt;/code&gt; no lo invoca con un SDK →  tiene que &lt;strong&gt;firmar la petición HTTP con SigV4&lt;/strong&gt; usando el servicio &lt;code&gt;lambda&lt;/code&gt;, e incluir siempre la cabecera &lt;code&gt;x-amz-content-sha256&lt;/code&gt; (Lambda la usa como hash del payload en vez de leer el body).&lt;/p&gt;

&lt;p&gt;El rol de &lt;code&gt;orders&lt;/code&gt; necesita el permiso &lt;code&gt;lambda:InvokeWebFunctionEndpoint&lt;/code&gt; sobre el endpoint de &lt;code&gt;inventory&lt;/code&gt;, y Lambda elimina la cabecera &lt;code&gt;Authorization&lt;/code&gt; antes de reenviar la request a la aplicación. Para endpoints multi-región la firma pasa a SigV4a.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La implementación:&lt;/strong&gt; Dos servicios Express en TypeScript, cada uno su propia Web Function con su rol de mínimo privilegio. &lt;code&gt;inventory&lt;/code&gt; guarda el stock en DynamoDB y hace la reserva atómica; &lt;code&gt;orders&lt;/code&gt; firma su llamada a &lt;code&gt;inventory&lt;/code&gt;, y si la reserva sale bien persiste el pedido en otra tabla. El detalle (despliegue ordenado, IAM y la receta de firma) está en el README del ejemplo.&lt;br&gt;
Hice dos experimentos de caga sobre el BFF. Los números salen de los eventos de CloudWatch (&lt;code&gt;invokeReport&lt;/code&gt; / &lt;code&gt;initReport&lt;/code&gt;).&lt;/p&gt;

&lt;h4&gt;
  
  
  Experimento 1 ➡ Carga: ¿cuánta CPU se cobra?
&lt;/h4&gt;

&lt;p&gt;&lt;em&gt;Objetivo:&lt;/em&gt; analizar con tráfico concurrente qué fracción del tiempo del entorno se factura como CPU. 500 pedidos, 50 en paralelo (no 500 a la vez: 500 en total).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Métrica&lt;/th&gt;
&lt;th&gt;Valor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pedidos (todos &lt;code&gt;201&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tiempo de reloj&lt;/td&gt;
&lt;td&gt;15 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Entornos creados&lt;/td&gt;
&lt;td&gt;61&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pico de concurrencia por entorno&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU activa de &lt;em&gt;serving&lt;/em&gt; (toda la flota)&lt;/td&gt;
&lt;td&gt;~49 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU de arranque (toda la flota)&lt;/td&gt;
&lt;td&gt;~22 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vida de servicio sumada&lt;/td&gt;
&lt;td&gt;~24.461 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Análisis:&lt;/em&gt; la CPU de serving (~49 s) es &lt;strong&gt;~0,2%&lt;/strong&gt; de la vida de servicio sumada (~24.461 s). Cuidado con la lectura: ese 99,8% restante &lt;strong&gt;no es todo "espera de I/O"&lt;/strong&gt;. La carga duró 15 s, pero cada entorno siguió vivo ~400 s hasta escalar a cero, así que la mayor parte es &lt;strong&gt;ocio&lt;/strong&gt; del entorno después de la carga, no espera por request. El punto importante, ese tiempo no se cobra como CPU (la memoria sí se cobra mientras el entorno exista). Medir la espera de I/O por request requiere los eventos &lt;code&gt;requestComplete&lt;/code&gt; (nivel DEBUG), que no lo activé acá.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fprod-assets.cosmic.aws.dev%2Fa%2F3Kmt03rgqsf2FHLA1ytgI7Sofw0%2Fexp1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fprod-assets.cosmic.aws.dev%2Fa%2F3Kmt03rgqsf2FHLA1ytgI7Sofw0%2Fexp1.webp" title="Experimento 1" alt="Experiment 1" width="799" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⭐ &lt;em&gt;Un detalle de la concurrencia:&lt;/em&gt; 50 pedidos en paralelo deberían caber en pocos entornos (uno atiende hasta 64), pero se abrieron 61 con un pico de solo 2 por entorno. No es un parámetro que faltó setear como &lt;code&gt;maxConcurrencyPerEnvironment&lt;/code&gt; porque solo fija el techo. Por debajo, Lambda usa un &lt;strong&gt;límite dinámico por entorno&lt;/strong&gt; que ajusta según el uso de recursos, y lo baja cuando cada request consume CPU (acá, la firma SigV4). Con poca concurrencia por entorno, Lambda abre más entornos, y cada uno paga su arranque: eso explica el costo del Experimento 2. La ventaja de "menos entornos por concurrencia compartida" frente a una Event Function es real, pero el reparto lo decide la plataforma, no tu config.&lt;/p&gt;

&lt;h4&gt;
  
  
  Experimento 2 ➡ ¿Cuánto cuesta un pedido?
&lt;/h4&gt;

&lt;p&gt;&lt;em&gt;Objetivo:&lt;/em&gt; separar el costo de arranque del trabajo real por pedido. Dos corridas: 1 pedido en un entorno frío, y 200 pedidos secuenciales en el mismo entorno caliente.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Corrida&lt;/th&gt;
&lt;th&gt;Pedidos&lt;/th&gt;
&lt;th&gt;CPU total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Entorno frío&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;~780 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Entorno caliente&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;~2.600 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Análisis:&lt;/em&gt; el costo marginal por pedido es &lt;code&gt;(2.600 − 780) / 199 ≈ 9 ms&lt;/code&gt;. O sea, un pedido en caliente cuesta &lt;strong&gt;~9 ms&lt;/strong&gt; de CPU (SigV4 + JSON + la parte de CPU del I/O), pero el &lt;strong&gt;primer&lt;/strong&gt; pedido de un entorno frío cuesta &lt;strong&gt;~780 ms&lt;/strong&gt; (arranque de Node + carga del SDK + primera conexión), unas 85 veces más. En una Web Function, lo que domina el costo de CPU es &lt;strong&gt;cuántos entornos se abren&lt;/strong&gt;, no cuántos pedidos se atienden.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fprod-assets.cosmic.aws.dev%2Fa%2F3KmtM9C913xa1FVk9URIkO3eh0q%2Fexp2.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fprod-assets.cosmic.aws.dev%2Fa%2F3KmtM9C913xa1FVk9URIkO3eh0q%2Fexp2.webp" title="Experiment 2" alt="Experiment 2" width="800" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusiones
&lt;/h2&gt;

&lt;p&gt;Lambda Web Functions no viene a reemplazar a la Lambda de siempre, suma una forma más, junto a las Event Functions y las durable functions. Su enfoque es claro: cargas que son, en esencia, un servidor HTTP atendiendo tráfico concurrente que pasa la mayor parte del tiempo esperando I/O. Ahí el modelo encaja muy bien.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Las ventajas que comprobé en el playground&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Código de servidor común (Express) sin adaptadores, que corre igual en local y en AWS.&lt;/li&gt;
&lt;li&gt;Concurrencia por entorno: cada entorno atiende varias peticiones mientras esperan I/O.&lt;/li&gt;
&lt;li&gt;Facturación por CPU activa: en la prueba del BFF, la CPU de serving fue ~0,2% de la vida de los entornos; la espera de I/O casi no se cobra.&lt;/li&gt;
&lt;li&gt;Streaming nativo (el SSE del agente) sin configuración extra.&lt;/li&gt;
&lt;li&gt;Controles en el endpoint (&lt;code&gt;rateLimit&lt;/code&gt;, &lt;code&gt;maxEnvironments&lt;/code&gt;) para proteger a las dependencias que están detrás, como una base de datos.&lt;/li&gt;
&lt;li&gt;Comunicación privada entre servicios con &lt;code&gt;IamAuth&lt;/code&gt;, aunque exige firmar las peticiones a mano con SigV4.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Sobre migrar&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Si ya tenés una app Express o Hono corriendo en Lambda con un adaptador, la migración es mayormente quitar el adaptador y escuchar en &lt;code&gt;AWS_LAMBDA_HTTP_ENDPOINT&lt;/code&gt;. Si viene de handlers por evento, hay que reescribirla como servidor. En los dos casos no conviene cambiar todo de golpe: las revisiones y los pesos por revisión del endpoint permiten hacer un canary (mandar un porcentaje del tráfico a la versión nueva y volver atrás si algo falla), que es lo que recomienda la guía de buenas prácticas para producción. Y antes de migrar, conviene preguntarse si hay algo para ganar: un endpoint de bajo volumen con request/response simple no mejora.&lt;/p&gt;

&lt;p&gt;Un punto importante, aunque lo nombre muchas veces en estas notas, es que hoy solo ofrece runtime con node.js.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Medir antes de decidir&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Los números del playground salen de una simulación. Sirven para entender el modelo, no para cotizar. La documentación recomienda hacer pruebas de carga con las mismas dependencias que en producción, subiendo la concurrencia hasta ver qué se agota primero (latencia, CPU, memoria o la base de datos), y revisar antes las cuotas de la cuenta y de la región. Con esos resultados se ajustan &lt;code&gt;maxConcurrencyPerEnvironment&lt;/code&gt;, &lt;code&gt;maxEnvironments&lt;/code&gt; y &lt;code&gt;rateLimit&lt;/code&gt;. El costo real hay que verlo en la factura: la memoria se cobra durante toda la vida del entorno, y lo que más mueve la CPU no es el trabajo por pedido (~9 ms en caliente en el BFF) sino cuántos entornos se abren, porque cada uno paga su arranque (~780 ms el primer pedido de un entorno frío).&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Lecciones aprendidas&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;La plataforma importa tu módulo a través de un wrapper, no lo ejecuta como entrypoint: el servidor tiene que arrancar al importarse.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/ping&lt;/code&gt; está reservado por la plataforma; para el health check hay que usar otra ruta.&lt;/li&gt;
&lt;li&gt;Toda dependencia que use el código, incluido el AWS SDK, tiene que estar declarada en el &lt;code&gt;package.json&lt;/code&gt;; si no, falla recién en runtime.&lt;/li&gt;
&lt;li&gt;No hay afinidad de sesión y el entorno puede desaparecer en cualquier momento: el estado va afuera (S3, DynamoDB).&lt;/li&gt;
&lt;li&gt;Los log groups se crean sin expiración; conviene fijar la retención.&lt;/li&gt;
&lt;li&gt;Con SAM, &lt;code&gt;CodeUri&lt;/code&gt; tiene que ser un objeto en un bucket S3 versionado: &lt;code&gt;sam build&lt;/code&gt; no empaqueta código local para este recurso.&lt;/li&gt;
&lt;li&gt;Es un servicio nuevo: hacen falta versiones recientes del AWS CLI y de SAM CLI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;
  
  
  Referencias
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://builder.aws.com/content/3KeP5Ow0BZUP6u2G9PPjskuCtPi/lambda-web-functions-because-your-express-app-deserves-better-than-a-lambda-adapter" rel="noopener noreferrer"&gt;Lambda Web Functions: because your Express app deserves better than a Lambda adapter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://edjgeek.com/blog/why-i-m-excited-about-lambda-web-functions/" rel="noopener noreferrer"&gt;Why I'm Excited About AWS Lambda Web Functions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/web-functions.html" rel="noopener noreferrer"&gt;AWS Lambda Web Functions — documentación oficial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-webfunction.html" rel="noopener noreferrer"&gt;AWS::Serverless::WebFunction — recurso de AWS SAM&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>serverless</category>
      <category>aws</category>
      <category>lambda</category>
      <category>http</category>
    </item>
    <item>
      <title>Serverless Highlights: October 2026 Week 1 AWS Announcements</title>
      <dc:creator>Nadtakan Futhoem (Nad)</dc:creator>
      <pubDate>Fri, 16 Oct 2026 18:24:40 +0000</pubDate>
      <link>https://dev.to/aws-builders/serverless-highlights-october-2026-week-1-aws-announcements-32h7</link>
      <guid>https://dev.to/aws-builders/serverless-highlights-october-2026-week-1-aws-announcements-32h7</guid>
      <description>&lt;p&gt;&lt;em&gt;📅 Covering AWS announcements from **Oct 10, 2026, 00:00 – Oct 16, 2026, 23:59&lt;/em&gt;* (UTC).*&lt;/p&gt;

&lt;p&gt;This week in AWS, we saw exciting developments across the serverless landscape! From new regional availability and feature enhancements to innovative use cases and integrations, the announcements promise to empower serverless developers like never before.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI and Machine Learning Enhancements
&lt;/h2&gt;

&lt;h3&gt;
  
  
  AI AgentCore and Bedrock
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/10/amazon-bedrock-agentcore/" rel="noopener noreferrer"&gt;Web Search on Amazon Bedrock AgentCore expands to AWS GovCloud and 3 Regions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/10/bring-sagemaker-ai-expertise-to-your-coding-agent/" rel="noopener noreferrer"&gt;Bring deep SageMaker AI expertise to your coding agent with the AWS AI/ML skill in the Agent Toolkit for AWS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/architecture/agentic-antimicrobial-resistance-surveillance-with-aws-healthomics-and-amazon-bedrock-agentcore/" rel="noopener noreferrer"&gt;Agentic antimicrobial resistance surveillance with AWS HealthOmics and Amazon Bedrock AgentCore&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/10/agentcore-memory-consumption-based-pricing/" rel="noopener noreferrer"&gt;Announcing consumption-based pricing for short-term memory in Amazon Bedrock AgentCore&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  AI and Data Processing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/architecture/how-dynata-scaled-ai-across-the-development-lifecycle-with-ai-dlc-and-kiro-ide/" rel="noopener noreferrer"&gt;Dynata scaled AI across the development lifecycle with AI-DLC and Kiro IDE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/architecture/stopping-business-email-compromise-with-ai-driven-email-security-how-vipre-protects-modern-enterprises-on-aws/" rel="noopener noreferrer"&gt;Stopping business email compromise with AI-driven email security: How VIPRE protects modern enterprises on AWS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Data and Storage Enhancements
&lt;/h2&gt;

&lt;h3&gt;
  
  
  DynamoDB
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/10/amazon-dynamodb-streams-timestamp-processing" rel="noopener noreferrer"&gt;Amazon DynamoDB Streams now supports timestamp-based stream processing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/10/redshift-long-term-system-table-retention-expansion/" rel="noopener noreferrer"&gt;Amazon Redshift long-term system table retention with Amazon S3 Tables is now available in additional AWS Regions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  S3 and EMR
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/10/amazon-s3-vercel-marketplace-v0" rel="noopener noreferrer"&gt;Amazon S3 is now available on the Vercel Marketplace and v0 by Vercel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/10/amazon-emr-data-quality-spark" rel="noopener noreferrer"&gt;Amazon EMR now supports native data quality checks in Apache Spark jobs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lambda Enhancements
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Durable Functions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/10/aws-lambda-durable-go-sdk-support/" rel="noopener noreferrer"&gt;AWS Lambda Durable Execution SDK for Go is now generally available&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/10/aws-lambda-web-functions/" rel="noopener noreferrer"&gt;AWS Lambda announces Lambda Web Functions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/aws/announcing-aws-lambda-web-functions-for-fast-consistent-web-app-performance-anywhere/" rel="noopener noreferrer"&gt;Announcing AWS Lambda Web Functions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/10/lambda-recursive-loops-eventbridge-custom" rel="noopener noreferrer"&gt;AWS Lambda recursive loop detection now supports Amazon EventBridge Custom event buses&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  General Lambda
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/10/aws-lambda-durable-gen-limit-increase/" rel="noopener noreferrer"&gt;AWS Lambda durable functions increases durable operation and checkpoint payload limits&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Application Recovery and Management
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/10/region-switch-target-health-ssm-lambda/" rel="noopener noreferrer"&gt;ARC Region switch adds four new capabilities&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/architecture/scope-workloads-and-share-resilience-policies-with-aws-resilience-hub/" rel="noopener noreferrer"&gt;Scope workloads and share resilience policies with AWS Resilience Hub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Security and Compliance
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/10/security-hub-cspm-cri-profile-v22/" rel="noopener noreferrer"&gt;AWS Security Hub CSPM now supports the Cyber Risk Institute profile version 2.2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/10/secrets-manager-managed-external-secrets-databricks-twilio/" rel="noopener noreferrer"&gt;AWS Secrets Manager adds managed external secrets support for Databricks and Twilio&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Regional Availability and Integrations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/10/amazon-msk-connect-aws-gsr-additional-aws-regions" rel="noopener noreferrer"&gt;Amazon MSK Connect and AWS Glue Schema Registry are now available in the AWS European Sovereign Cloud (Germany) region&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/10/cloudfront-lambda-web-functions-origin/" rel="noopener noreferrer"&gt;Amazon CloudFront now supports AWS Lambda Web Functions as an origin&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Your Serverless Guide to re:Invent 2026
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/compute/your-serverless-attendees-guide-to-aws-reinvent-2026/" rel="noopener noreferrer"&gt;Your serverless attendee’s guide to AWS re:Invent 2026&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Disaster Recovery and Multi-Region Solutions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/architecture/multi-region-disaster-recovery-for-amazon-api-gateway-websocket-apis/" rel="noopener noreferrer"&gt;Multi-region disaster recovery for Amazon API Gateway WebSocket APIs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/architecture/how-uts-established-a-modern-curriculum-data-services-foundation-on-aws/" rel="noopener noreferrer"&gt;How UTS established a modern curriculum data services foundation on AWS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  AWS Agent Registry
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/10/aws-agent-registry-custom-metadata-registry-records" rel="noopener noreferrer"&gt;AWS Agent Registry now supports custom metadata for registry records&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this means
&lt;/h2&gt;

&lt;p&gt;This week's announcements showcase AWS's continuous commitment to enhancing serverless capabilities and expanding regional availability. From AI and ML advancements to data processing improvements and security enhancements, developers now have more tools and options than ever to build, deploy, and manage serverless applications efficiently and securely. Whether you're looking to scale AI across your development lifecycle, improve data quality checks, or secure your applications with enhanced secrets management, AWS has you covered.&lt;/p&gt;




&lt;h2&gt;
  
  
  📦 New Releases This Week
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Additional announcement(s) from 2026-10-10 – 2026-10-16 (UTC) not covered above:&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What's New
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/amazon-dynamodb-streams-timestamp-processing" rel="noopener noreferrer"&gt;Amazon DynamoDB Streams now supports timestamp-based stream processing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/10/amazon-mwaa-ai-agent-skills/" rel="noopener noreferrer"&gt;Amazon Managed Workflows for Apache Airflow now offers AI agent skills&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/10/amazon-emr-data-quality-spark/" rel="noopener noreferrer"&gt;Amazon EMR now supports native data quality checks in Apache Spark jobs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  AWS News Blog
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/aws/amazon-s3-vercel-marketplace-v0/" rel="noopener noreferrer"&gt;Amazon S3 is now available on the Vercel Marketplace and v0 by Vercel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/aws/aws-weekly-roundup-prime-day-2026-by-the-numbers-claude-haiku-5-5-amazon-nova-2-5-sonic-and-more-october-13-2026/" rel="noopener noreferrer"&gt;AWS Weekly Roundup: Prime Day 2026 by the numbers, Claude Haiku 5.5, Amazon Nova 2.5 Sonic, and more (October 13, 2026)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Architecture Blog
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/architecture/bulk-executor-for-amazon-dynamodb-part-2-lessons-we-learned-the-hard-way/" rel="noopener noreferrer"&gt;Bulk Executor for Amazon DynamoDB, Part 2: Lessons we learned the hard way&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Compute Blog
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/compute/exploring-aws-lambda-web-functions-for-building-web-applications/" rel="noopener noreferrer"&gt;Exploring AWS Lambda Web Functions for building web applications&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;blockquote&gt;
&lt;p&gt;🤖 &lt;em&gt;This post was automatically generated by AI using Amazon Bedrock (model &lt;code&gt;amazon.nova-lite-v1:0&lt;/code&gt;). It summarizes recent AWS announcements and may contain inaccuracies — please verify details against the official AWS sources linked above.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>announcements</category>
      <category>news</category>
    </item>
    <item>
      <title>I measured the speed and monthly cost of Amazon S3 Files, which lets you use S3 as files (and how it differs from FSx for ONTAP)</title>
      <dc:creator>Yoshiki Fujiwara(藤原 善基)@AWS Community Builder</dc:creator>
      <pubDate>Fri, 16 Oct 2026 11:20:30 +0000</pubDate>
      <link>https://dev.to/aws-builders/i-measured-the-speed-and-monthly-cost-of-amazon-s3-files-which-lets-you-use-s3-as-files-and-how-1970</link>
      <guid>https://dev.to/aws-builders/i-measured-the-speed-and-monthly-cost-of-amazon-s3-files-which-lets-you-use-s3-as-files-and-how-1970</guid>
      <description>&lt;h2&gt;
  
  
  Conclusion first: the one-client speed limit, scaling with client count, and a monthly cost order that splits by object size
&lt;/h2&gt;

&lt;p&gt;Amazon S3 Files lets you open, read, and write the data in S3 like an ordinary folder. With plain S3, a program has to call S3-specific APIs. With S3 Files, tools built to open files can work on your S3 data as they are.&lt;/p&gt;

&lt;p&gt;This post is a record of measurements and estimates for people who do not run storage performance tests as a routine job, to help decide whether S3 Files belongs in a design. I built the environment myself in ap-northeast-1 and measured speed on 2026-10-10. The monthly cost is not a measurement. It is arithmetic from list prices read on 2026-08-09. When I re-read the 2026-08-09 record with the Price List API on 2026-10-16, the values were the same (it covers 10 items and does not include the two PUT request prices).&lt;/p&gt;

&lt;p&gt;Three things came out of it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One server has a read speed limit. On a c5n.9xlarge (a 36 vCPU server) it was about 1.1 GB per second (&lt;strong&gt;1,104.9 MB/s&lt;/strong&gt;). It stops when the relay program running on the server (efs-proxy) uses up one CPU core (96.8 to 97.9%). One run.&lt;/li&gt;
&lt;li&gt;Adding servers raises the total. Going from 1 to 8 c5n.2xlarge servers raised the total from 784.4 MB/s to &lt;strong&gt;6,282.6 MB/s&lt;/strong&gt; (8.01x). All servers read the same 600 GiB file.&lt;/li&gt;
&lt;li&gt;Under this model's base assumptions, the monthly cost order against FSx for ONTAP splits by object size. Amazon FSx for NetApp ONTAP (FSx for ONTAP below) is an AWS managed service that provides NFS and SMB shared folders, and with S3 Access Points attached it also serves the same data through the S3 API. I fixed the stored volume at 20 TiB and estimated five patterns that differ in monthly read and write volume (P1, P4, P5, P6, P8; the contents are in the table in the chapter "Monthly cost estimate"). At 64 KiB, FSx for ONTAP was cheaper in all five ($1,783.11 to $2,218.13 a month, against $7,928.32 to $12,030.81 via S3 Files). At 4 MiB, S3 Files was cheaper in all five ($512.14 to $543.52 a month, against $1,781.28 to $1,788.08 on FSx for ONTAP).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The monthly amounts are subtotals of the main line items for ap-northeast-1, with prices read on 2026-08-09. A main-line-item subtotal is the sum of storage, S3 Files high-performance storage (a low-latency layer inside the file system; you pay storage charges for what it holds), fixed bandwidth, requests, and so on, and it leaves out any item that is not in the tables. They are price x quantity arithmetic, not measurements, and they are not invoices. The FSx for ONTAP capacity pool (the tier that holds data that does not fit on SSD) read request count is an assumed value that I have not confirmed. The P5 write volume and the read and write volumes of P1, P6, and P8 are placeholders with no source.&lt;/p&gt;

&lt;p&gt;Because the order was the same in all five patterns, what splits the order in this model is the object size, not the read and write volume. The candidate for the dividing line is the S3 Files import threshold (the file-size boundary for importing into high-performance storage; default 128 KiB). I estimated only two sizes, 64 KiB and 4 MiB, and I did not estimate the sizes at the boundary.&lt;/p&gt;

&lt;p&gt;The takeaways: when one server is not fast enough, add servers (confirmed up to 8). Under the base assumptions, FSx for ONTAP was cheaper at 64 KiB and S3 Files was cheaper at 4 MiB. The order can flip when the assumptions change, so replace them with your own values and check. The assumptions to replace are the share of data on the capacity pool, the storage efficiency of the SSD tier, the active share of S3 Files high-performance storage, the S3 Files import threshold, and the FSx for ONTAP deployment. This post does not give the values at which the order flips. The estimate itself is in the chapter "Monthly cost estimate".&lt;/p&gt;

&lt;h3&gt;
  
  
  How to read this post by goal
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You only want the conclusion: this chapter is enough.&lt;/li&gt;
&lt;li&gt;You want to know whether it works in your setup: "The value of S3 Files and four prerequisites"&lt;/li&gt;
&lt;li&gt;The mount does not proceed: "A hung mount"&lt;/li&gt;
&lt;li&gt;You want to know how it differs from FSx for ONTAP: "Comparing two ways to use the same data through two entry points"&lt;/li&gt;
&lt;li&gt;You want the cost: "Monthly cost estimate"&lt;/li&gt;
&lt;li&gt;You want to know which setup to consider first: "Choosing between S3 bucket first and file system first" at the end of the "Monthly cost estimate" chapter, and "Closing"&lt;/li&gt;
&lt;li&gt;You want the measurement conditions and limits: "The single-mount ceiling", "An aggregate that scales with client count", "An invalid warm / cold comparison", "Status of the four stated values", "What I have not measured"&lt;/li&gt;
&lt;li&gt;You want to reproduce it: "Reproducing it yourself"&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;There are times when you want to handle S3 data as files: an existing script that opens the files in a folder one by one, or a business tool built around a shared folder. Amazon S3 Files became generally available in April 2026, and it lets you mount an S3 bucket as an NFS file system (the network shared-folder mechanism of Linux) without migrating data.&lt;/p&gt;

&lt;p&gt;Before using it, I think you wonder about three things.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is it good for, in the end?&lt;/li&gt;
&lt;li&gt;How fast is it, and what does it cost?&lt;/li&gt;
&lt;li&gt;How does it differ from a file-server type of service (FSx for ONTAP)?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post answers the second and third. The speeds are values I measured in my own environment, and the monthly cost is a unit price x quantity estimate. For the differences from FSx for ONTAP, I put my past measurement records (the environment differs per measurement) and the official documentation side by side in a table. I do not cover the first, the introduction. The &lt;a href="https://dev.classmethod.jp/ja/articles/amazon-s3-files-ga-mount-and-compare-efs/" rel="noopener noreferrer"&gt;Classmethod article&lt;/a&gt; (Japanese) compares I/O performance and sync lag with Amazon EFS, and the &lt;a href="https://blog.serverworks.co.jp/s3-mount-services-comparison" rel="noopener noreferrer"&gt;Serverworks article&lt;/a&gt; (Japanese) sorts out five services, including Mountpoint for Amazon S3.&lt;/p&gt;

&lt;p&gt;In the previous post (&lt;a href="https://hakobiya.hatenablog.com/entry/fsxn-s3burst-throughput-ceilings-three-paths" rel="noopener noreferrer"&gt;S3 Burst Part 2: "The throughput ceilings of three paths"&lt;/a&gt;) I compared three paths, S3 Files among them, side by side and asked where each one tops out. This time I narrow down to S3 Files alone. Last time the question was which path tops out where. This time it is how S3 Files behaves as a file system.&lt;/p&gt;

&lt;p&gt;The mount failed before I could measure anything, so I recorded the cause first. Someone else can hit the same wall. After reading, you can decide, on numbers that come with their environments, whether S3 Files is a candidate, how far the number of servers can carry your speed, and which of S3 Files and FSx for ONTAP with S3 Access Points (the S3 API entry point that you attach to a volume) fits better.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I cover / do not cover
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Covered
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The prerequisites of S3 Files and the value AWS states for it. Prerequisites decide first whether you can use it at all.&lt;/li&gt;
&lt;li&gt;Why the mount failed. It was missing permissions, not the network, and knowing in advance avoids it.&lt;/li&gt;
&lt;li&gt;The single-mount ceiling (c5n.9xlarge). The per-client ceiling is the starting point of capacity planning.&lt;/li&gt;
&lt;li&gt;Aggregate growth with client count (c5n.2xlarge, 1 to 8 clients). It shows how far clients can carry you past one mount's limit.&lt;/li&gt;
&lt;li&gt;The comparison with FSx for ONTAP S3 Access Points. Both give you the same data through files and the S3 API, but the source of truth, the speed of propagation, the growth with client count, and the billing items differ.&lt;/li&gt;
&lt;li&gt;The monthly cost estimate. With the stored volume fixed at 20 TiB, five read and write patterns, and two object sizes, I line up the main-line-item subtotals of S3 Files and FSx for ONTAP. It separates this model's result, in which the size splits the order, from the assumptions under which the order can flip.&lt;/li&gt;
&lt;li&gt;The accounting assumption for S3 Files metadata charges, and one observation. Whether including them in the subtotal matches the bill is unconfirmed.&lt;/li&gt;
&lt;li&gt;Reading data on the capacity pool. It separates the existence of billing items from how far a latency observation can be read.&lt;/li&gt;
&lt;li&gt;How my warm / cold comparison failed. Warm means reading data that sits in S3 Files' high-performance storage; cold means reading straight from the bucket. Someone repeating the measurement can fall into the same trap.&lt;/li&gt;
&lt;li&gt;Which of the four values AWS states I could confirm, and which I could not. It separates what is safe to base an adoption on.&lt;/li&gt;
&lt;li&gt;What I have not measured. It lets you judge where the numbers apply.&lt;/li&gt;
&lt;li&gt;How to reproduce it in your environment. It points to the runbooks so you can follow the measurements.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Not covered
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;An introduction to S3 Files, and how it differs from Mountpoint for Amazon S3 and Amazon EFS. Articles already cover that, and it is outside this post's subject, measured speed and monthly cost.&lt;/li&gt;
&lt;li&gt;Which configuration is faster. The paths were measured in different environments (NFS version, instance type, configuration), so no same-conditions winner exists.&lt;/li&gt;
&lt;li&gt;Measurements that separate the propagation conditions. The window of about 60 seconds between writing a file and reading it through the S3 API appears as two rows in the comparison table of "Comparing two ways to use the same data through two entry points", and I explain it in four stages. Measurements that separate conditions, such as long continuous appends and concurrent updates, exist only as a plan and have no measured results. The conditions of that window are in &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-measured.md#L42-L46" rel="noopener noreferrer"&gt;another verification record&lt;/a&gt; (Japanese). It is outside this post's subject, which is throughput and client count, and that measurement covers a single condition.&lt;/li&gt;
&lt;li&gt;The values at which the order flips. I did not calculate where the order flips at the boundary object size, the capacity pool share, and so on.&lt;/li&gt;
&lt;li&gt;Invoiced amounts. The monthly cost is a unit price x quantity estimate, and I have not confirmed that it matches a bill.&lt;/li&gt;
&lt;li&gt;A correct same-conditions measurement of the warm / cold difference. The first comparison was invalid and I have not re-measured.&lt;/li&gt;
&lt;li&gt;Multi-AZ configurations, clients outside AWS, and tiering differences inside a VPC. I measured in a single-AZ environment in ap-northeast-1. The chapter "What I have not measured" lists the scope in a table.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Terms used in this post
&lt;/h2&gt;

&lt;p&gt;Each term is explained at first use in the text. This table is an index for rereading.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Where it appears&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;S3 Files&lt;/td&gt;
&lt;td&gt;A service that mounts an S3 bucket as an NFS file system. Built on Amazon EFS; the bucket remains the source of truth&lt;/td&gt;
&lt;td&gt;All chapters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-performance storage&lt;/td&gt;
&lt;td&gt;The low-latency layer inside the S3 Files file system. It holds the data and metadata of files in active use, and you pay storage charges for what it holds&lt;/td&gt;
&lt;td&gt;"The value of S3 Files and four prerequisites", "Comparing two ways to use the same data through two entry points", "Monthly cost estimate", "An invalid warm / cold comparison"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;efs-proxy&lt;/td&gt;
&lt;td&gt;A local proxy that runs on the mounting client's EC2 instance. The mount target is 127.0.0.1, and in the verification records this one process handles TLS termination and NFS translation&lt;/td&gt;
&lt;td&gt;"The value of S3 Files and four prerequisites", "A hung mount", "The single-mount ceiling", "An aggregate that scales with client count", "Comparing two ways to use the same data through two entry points"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mount target&lt;/td&gt;
&lt;td&gt;The NFS endpoint of the file system that you create in a VPC (port 2049). One per AZ at most&lt;/td&gt;
&lt;td&gt;"The value of S3 Files and four prerequisites", "A hung mount"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nconnect&lt;/td&gt;
&lt;td&gt;A Linux NFS mount option that opens several TCP connections to one server. S3 Files does not support it, and specifying it hung the mount&lt;/td&gt;
&lt;td&gt;"The value of S3 Files and four prerequisites", "The single-mount ceiling", "Comparing two ways to use the same data through two entry points"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;warm / cold&lt;/td&gt;
&lt;td&gt;In this post, used only for S3 Files reads. Warm is a read of data held in high-performance storage; cold is a read straight from the bucket. For FSx for ONTAP I write "on SSD" and "on the capacity pool"&lt;/td&gt;
&lt;td&gt;"An invalid warm / cold comparison", "Status of the four stated values", "What I have not measured"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capacity pool&lt;/td&gt;
&lt;td&gt;The tier where FSx for ONTAP keeps data that does not fit on SSD. Its storage price is lower than SSD (Single-AZ, ap-northeast-1: $0.0238 vs. $0.15 per GB-month, read 2026-08-09)&lt;/td&gt;
&lt;td&gt;"Comparing two ways to use the same data through two entry points", "Monthly cost estimate", "What I have not measured"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tiering All&lt;/td&gt;
&lt;td&gt;One of the tiering policies of an FSx for ONTAP volume. It places the data on the capacity pool&lt;/td&gt;
&lt;td&gt;"Comparing two ways to use the same data through two entry points", "Monthly cost estimate"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FlexCache / Origin&lt;/td&gt;
&lt;td&gt;FlexCache is an ONTAP feature in which a Cache volume holds only the data read from the Origin volume that owns the original data. This post uses it only to say that the FSx for ONTAP propagation measurement read the Origin volume directly&lt;/td&gt;
&lt;td&gt;"Comparing two ways to use the same data through two entry points"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Access Points&lt;/td&gt;
&lt;td&gt;In this post, the S3 API entry point attached to an FSx for ONTAP volume. The same data can be read and written over NFS / SMB and the S3 API&lt;/td&gt;
&lt;td&gt;"Comparing two ways to use the same data through two entry points", "Monthly cost estimate"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Import threshold&lt;/td&gt;
&lt;td&gt;The size at which S3 Files decides whether to import a file into high-performance storage (&lt;code&gt;sizeLessThan&lt;/code&gt;). The default is 128 KiB, and sizes below it are imported. Exactly 128 KiB is treated as not imported&lt;/td&gt;
&lt;td&gt;"The value of S3 Files and four prerequisites", "Monthly cost estimate", "An invalid warm / cold comparison", "Closing"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pattern (P1, P4, P5, P6, P8)&lt;/td&gt;
&lt;td&gt;A name for a combination of monthly read and write volumes. The numbers follow the cost model's numbering, and the figures and tables carry five of them (P2, P3, and P7 are left out because they cost the same as P4 and P6)&lt;/td&gt;
&lt;td&gt;"Monthly cost estimate", "Closing"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Working set / working-set ratio&lt;/td&gt;
&lt;td&gt;The working set is the part that is read within the month. The working-set ratio is its share of the stored volume (10% for P4)&lt;/td&gt;
&lt;td&gt;"Monthly cost estimate"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Churn rate&lt;/td&gt;
&lt;td&gt;The share of the stored volume that is rewritten in a month (5% for P5)&lt;/td&gt;
&lt;td&gt;"Monthly cost estimate"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Main-line-item subtotal&lt;/td&gt;
&lt;td&gt;The sum of storage, S3 Files high-performance storage, fixed bandwidth, requests, and so on. Items not in the tables are not included. It is price x quantity arithmetic, not a measurement, and not an invoice&lt;/td&gt;
&lt;td&gt;"Monthly cost estimate", "Closing"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Placeholder&lt;/td&gt;
&lt;td&gt;A value with no source, put in to make the arithmetic work. It is not taken from a real workload&lt;/td&gt;
&lt;td&gt;"Monthly cost estimate"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VDBENCH&lt;/td&gt;
&lt;td&gt;A tool that generates storage I/O load (distributed by Oracle). The S3 Files measurements here used 5.04.07&lt;/td&gt;
&lt;td&gt;"The single-mount ceiling", "An aggregate that scales with client count"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o_direct&lt;/td&gt;
&lt;td&gt;A setting that reads and writes without the OS page cache. In VDBENCH it is &lt;code&gt;openflag=o_direct&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;"The single-mount ceiling", "An aggregate that scales with client count"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;disjoint / same&lt;/td&gt;
&lt;td&gt;Disjoint: each client reads a non-overlapping range. Same: all clients read the same file&lt;/td&gt;
&lt;td&gt;"An aggregate that scales with client count"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;shared epoch&lt;/td&gt;
&lt;td&gt;All clients wait until the same UNIX time (90 seconds after launch) and start measuring together&lt;/td&gt;
&lt;td&gt;"An aggregate that scales with client count"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The value of S3 Files and four prerequisites
&lt;/h2&gt;

&lt;p&gt;S3 Files lets Linux compute mount a bucket as an NFS file system and read and write it while the bucket stays the source of truth. It is built on Amazon EFS and became generally available in 34 regions in April 2026 (&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/04/amazon-s3-files/" rel="noopener noreferrer"&gt;What's New&lt;/a&gt;). Four prerequisites decide first whether you can use it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Four prerequisites
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prerequisite&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bucket versioning&lt;/td&gt;
&lt;td&gt;Required. Encryption is SSE-S3 or SSE-KMS&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-prereq-policies.html" rel="noopener noreferrer"&gt;Prerequisites for S3 Files&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client compute&lt;/td&gt;
&lt;td&gt;Linux on AWS (EC2 / Lambda / ECS / EKS / Fargate / Batch). I could not find documentation for mounting from outside AWS&lt;/td&gt;
&lt;td&gt;&lt;a href="https://aws.amazon.com/s3/features/files/" rel="noopener noreferrer"&gt;S3 Files product page&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protocol&lt;/td&gt;
&lt;td&gt;NFSv4.1 and NFSv4.2 only. NFSv3 and SMB are not listed as supported. nconnect (a Linux mount option that opens several TCP connections to one server) is unsupported; in my verification, specifying it timed out the mount after 90 seconds (one observation, c5n.2xlarge, 2026-09-02)&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-quotas.html" rel="noopener noreferrer"&gt;Unsupported features, limits, and quotas&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/throughput-iops-concurrency.md#L1175-L1179" rel="noopener noreferrer"&gt;verification record&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File-to-bucket propagation&lt;/td&gt;
&lt;td&gt;After you change a file, S3 Files waits about 60 seconds before copying it to the bucket&lt;/td&gt;
&lt;td&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-performance.html" rel="noopener noreferrer"&gt;Performance specifications&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  How the mount works
&lt;/h3&gt;

&lt;p&gt;When you mount, efs-proxy (the local proxy mentioned in the summary) starts on the client's EC2 instance. Your application mounts 127.0.0.1, and efs-proxy connects to the mount target in the VPC. The mount target is the NFS endpoint of the file system (port 2049), with a limit of one per AZ. In the verification records, this efs-proxy handles TLS termination and NFS translation in one process (&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md#L81-L84" rel="noopener noreferrer"&gt;verification record&lt;/a&gt;, Japanese). This structure sets the single-mount ceiling measured in a later chapter.&lt;/p&gt;

&lt;p&gt;Reads take one of two paths. Small files (under 128 KiB by default) are imported asynchronously into high-performance storage, the low-latency layer inside the file system, on first access, and later reads are fast. Reads of 1 MiB or more go straight to the bucket even when the data sits in high-performance storage. The source is &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-performance.html" rel="noopener noreferrer"&gt;Performance specifications&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Values AWS states
&lt;/h3&gt;

&lt;p&gt;The following four points are AWS's statements. I could get numbers for the second point, propagation time between files and the S3 API, and for the fourth point, single-mount throughput and aggregate growth with client count (up to 8). I did not measure the first, and the comparison for the third was invalid.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Existing file-based tools work as they are. The product page says "No code changes, no custom connectors, and no new APIs to learn" (&lt;a href="https://aws.amazon.com/s3/features/files/" rel="noopener noreferrer"&gt;product page&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;The same data is usable from both files and the S3 API at the same time. The product page's wording is "nothing to reconcile between two separate systems", meaning you do not have to reconcile two systems.&lt;/li&gt;
&lt;li&gt;Only frequently read data sits in high-performance storage, and you pay for what it holds. Reads of 1 MiB or more cost only the standard S3 GET price (&lt;a href="https://aws.amazon.com/s3/features/files/" rel="noopener noreferrer"&gt;product page&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Many clients can use it at once. The limit is 25,000 connections per file system, and the aggregate read throughput is "up to terabytes per second" in the AWS pages' wording (&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-quotas.html" rel="noopener noreferrer"&gt;quotas&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-performance.html" rel="noopener noreferrer"&gt;Performance specifications&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your requirements include NFSv3 or fixed SMB devices or Windows workflows, S3 Files drops out at the prerequisites table. The AWS documentation does not mention mounting from outside AWS that I could find. If you need it, check in your own environment before you put S3 Files on the candidate list. If none of those apply, read the following chapters on the mount failure, the single-mount ceiling, and client scaling, and use them for capacity planning.&lt;/p&gt;

&lt;h2&gt;
  
  
  A hung mount: missing CloudWatch permissions on the client's IAM role
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;mount -t s3files&lt;/code&gt; timed out every 15 seconds. The cause was not the network or DNS. &lt;strong&gt;The IAM role on the client EC2 instance lacked &lt;code&gt;logs:CreateLogGroup&lt;/code&gt; and &lt;code&gt;cloudwatch:PutMetricData&lt;/code&gt;.&lt;/strong&gt; Adding the permissions let the mount complete.&lt;/p&gt;

&lt;p&gt;The environment was a c5n.9xlarge (Amazon Linux 2023, &lt;code&gt;amazon-efs-utils&lt;/code&gt; 3.3.2) in a dedicated measurement VPC in ap-northeast-1 (a public subnet with an internet gateway). It happened while I prepared the measurement, and my troubleshooting notes carry no date. The throughput measurements in the same environment ran on 2026-10-10.&lt;/p&gt;

&lt;h3&gt;
  
  
  Symptom: timeouts every 15 seconds with a healthy network
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;efs-proxy: running, with a TCP connection to port 2049 on the mount target (ESTAB in &lt;code&gt;ss&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Reachability: both S3 on 443 and the service endpoint on 2049 were reachable&lt;/li&gt;
&lt;li&gt;Mount: &lt;code&gt;mount.nfs4 127.0.0.1:/&lt;/code&gt; still did not finish&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cause: efs-proxy restarting over and over because log creation failed
&lt;/h3&gt;

&lt;p&gt;The efs-proxy log contained these lines.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WARN  cw_publisher Failed to create log group: AccessDeniedException ... logs:CreateLogGroup
ERROR nfs::nfs_reader Error sending message batch to dispatcher DispatchingFailure
INFO  controller Proxy incarnation restarted, restart_count=3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before establishing the NFS connection, the mount helper tries to create a CloudWatch Logs log group (&lt;code&gt;/aws/efs/utils&lt;/code&gt;). When that call fails for lack of permissions, efs-proxy keeps restarting and the mount never finishes. An AWS article names the same behavior as the root cause when a mount hangs forever without an obvious reason (&lt;a href="https://repost.aws/articles/ARPg3C_iUzSkWA3l1gdUPUJA/mounting-amazon-s3-files-on-ec2-instances-in-private-subnets-without-internet-access" rel="noopener noreferrer"&gt;Mounting S3 Files on EC2 in private subnets&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;My subnet has an internet gateway, so CloudWatch Logs was reachable. Permissions were the blocker, not reachability. In a private subnet with no CloudWatch Logs VPC endpoint, unreachability produces the same symptom.&lt;/p&gt;

&lt;p&gt;Original record: &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-mount-pitfalls.md#L25-L61" rel="noopener noreferrer"&gt;Mount pitfalls (causes and fixes)&lt;/a&gt; (Japanese)&lt;/p&gt;

&lt;h3&gt;
  
  
  Fixes: any of three options works
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;th&gt;Fits when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Grant the IAM role &lt;code&gt;logs:CreateLogGroup&lt;/code&gt;, &lt;code&gt;logs:CreateLogStream&lt;/code&gt;, &lt;code&gt;logs:PutLogEvents&lt;/code&gt;, and &lt;code&gt;cloudwatch:PutMetricData&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;You have an internet gateway or CloudWatch endpoints and also want CloudWatch metrics. I used this one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Disable CloudWatch logging in the mount helper (&lt;code&gt;/etc/amazon/efs/s3files-utils.conf&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;You do not want to add permissions or endpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Create a CloudWatch Logs VPC endpoint (&lt;code&gt;com.amazonaws.&amp;lt;region&amp;gt;.logs&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;A private subnet with neither an internet gateway nor a NAT&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I did not pick option 2 because the next chapter's measurement uses efs-proxy's CloudWatch metrics (connectivity and byte counts) to isolate the cause of the ceiling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pre-mount checks: three items
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;IAM role: it carries the permissions from fix 1.&lt;/li&gt;
&lt;li&gt;botocore: installed with &lt;code&gt;dnf install python3-botocore&lt;/code&gt; (&lt;code&gt;pip3&lt;/code&gt; does not work). Without it, the mount either stopped with &lt;code&gt;access denied by server while mounting&lt;/code&gt;, or went through but CloudWatch metrics were unavailable. The two verification records show different symptoms, and neither records the condition that decides which one you get.&lt;/li&gt;
&lt;li&gt;Mount target: it is in the same AZ as the client, and the security group allows 2049 from the client.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check these three items before you mount, and decide beforehand whether to grant the CloudWatch permissions or disable logging.&lt;/p&gt;

&lt;h2&gt;
  
  
  The single-mount ceiling: one efs-proxy core saturated
&lt;/h2&gt;

&lt;p&gt;A single mount stopped at &lt;strong&gt;1,104.9 MB/s&lt;/strong&gt; for 1 MiB sequential reads (c5n.9xlarge, one run). The limiter was the efs-proxy process's CPU: one core of 36 vCPU was full (96.8 to 97.9%). As noted in the prerequisites chapter, S3 Files does not support nconnect, so no mount option can add connections to efs-proxy and move this ceiling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measurement conditions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Environment: ap-northeast-1, 2026-10-10 (UTC), single AZ, c5n.9xlarge (36 vCPU), Amazon Linux 2023&lt;/li&gt;
&lt;li&gt;Mount: &lt;code&gt;nfs4 vers=4.2&lt;/code&gt;, &lt;code&gt;rsize=wsize=1048576&lt;/code&gt;, &lt;code&gt;hard&lt;/code&gt;. The server is &lt;code&gt;127.0.0.1&lt;/code&gt; (through efs-proxy, TLS and IAM authentication always on)&lt;/li&gt;
&lt;li&gt;Load: VDBENCH 5.04.07 (an I/O load generator distributed by Oracle), 512 threads, &lt;code&gt;o_direct&lt;/code&gt; (bypasses the OS page cache)&lt;/li&gt;
&lt;li&gt;Data: a single uncompressible 600 GiB file. 60 seconds of warmup, then 300 seconds measured&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Results: four workloads
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;Throughput&lt;/th&gt;
&lt;th&gt;IOPS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 MiB sequential read&lt;/td&gt;
&lt;td&gt;1,104.9 MB/s&lt;/td&gt;
&lt;td&gt;1,105&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 MiB sequential write&lt;/td&gt;
&lt;td&gt;852.0 MB/s&lt;/td&gt;
&lt;td&gt;852&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 KiB random read&lt;/td&gt;
&lt;td&gt;61.9 MB/s&lt;/td&gt;
&lt;td&gt;15,839&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 KiB random write&lt;/td&gt;
&lt;td&gt;14.4 MB/s&lt;/td&gt;
&lt;td&gt;3,688&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each row is one run; I did not repeat any condition. Source of this table: &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md#L53-L61" rel="noopener noreferrer"&gt;Results (S-1 + S-2)&lt;/a&gt; (Japanese)&lt;/p&gt;

&lt;h3&gt;
  
  
  Isolating the cause: efs-proxy CPU saturated at one core
&lt;/h3&gt;

&lt;p&gt;I re-measured 1 MiB sequential reads for 60 seconds and sampled CPU three times.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sample&lt;/th&gt;
&lt;th&gt;efs-proxy CPU&lt;/th&gt;
&lt;th&gt;VDBENCH and java CPU&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;96.8%&lt;/td&gt;
&lt;td&gt;near 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;97.4%&lt;/td&gt;
&lt;td&gt;near 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;97.9%&lt;/td&gt;
&lt;td&gt;near 0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The re-measured throughput was 1,109.4 MB/s, 0.4% away from the main run's 1,104.9 MB/s. The mount target is a local proxy at &lt;code&gt;127.0.0.1&lt;/code&gt;, and efs-proxy handles TLS termination and NFS translation in one process, so one mount tops out at one core of that process. Source of this table: &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md#L72-L76" rel="noopener noreferrer"&gt;Ceiling confirmation&lt;/a&gt; (Japanese)&lt;/p&gt;

&lt;h3&gt;
  
  
  Differences by instance type: three values under different conditions
&lt;/h3&gt;

&lt;p&gt;Even on the same c5n.2xlarge the value changes with how I measured, so here are three side by side.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measurement&lt;/th&gt;
&lt;th&gt;Instance&lt;/th&gt;
&lt;th&gt;Conditions&lt;/th&gt;
&lt;th&gt;Read&lt;/th&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stream-count sweep&lt;/td&gt;
&lt;td&gt;c5n.2xlarge&lt;/td&gt;
&lt;td&gt;8 streams, 1 GiB per stream, &lt;code&gt;O_DIRECT&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;451.3 MB/s&lt;/td&gt;
&lt;td&gt;2026-09-02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One client in the client-count test&lt;/td&gt;
&lt;td&gt;c5n.2xlarge&lt;/td&gt;
&lt;td&gt;VDBENCH, 64 threads, 1 MiB sequential, 600 GiB file&lt;/td&gt;
&lt;td&gt;784.4 MB/s&lt;/td&gt;
&lt;td&gt;2026-10-10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single-mount test&lt;/td&gt;
&lt;td&gt;c5n.9xlarge&lt;/td&gt;
&lt;td&gt;VDBENCH, 512 threads, 1 MiB sequential, 600 GiB file&lt;/td&gt;
&lt;td&gt;1,104.9 MB/s&lt;/td&gt;
&lt;td&gt;2026-10-10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three differ in conditions, so this is not a comparison that changes only the instance type. I have no measurement that varies only the instance type under the same conditions. I also did not isolate whether efs-proxy CPU or the client's network bandwidth sets the roughly 785 MB/s per c5n.2xlarge. Source of this table: &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/throughput-iops-concurrency.md#L1139-L1150" rel="noopener noreferrer"&gt;Stream-count measurement&lt;/a&gt; (Japanese)&lt;/p&gt;

&lt;p&gt;When one mount is not enough bandwidth, design for adding clients (the next chapter).&lt;/p&gt;

&lt;h2&gt;
  
  
  An aggregate that scales with client count: 8.01x at 8 clients
&lt;/h2&gt;

&lt;p&gt;Going from 1 to 8 c5n.2xlarge clients raised the aggregate read from 784.4 MB/s to &lt;strong&gt;6,282.6 MB/s (8.01x)&lt;/strong&gt;. Per client it stayed roughly constant at 784.4 to 802.2 MB/s. That fits the structure in which each client has its own efs-proxy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measurement conditions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Environment: ap-northeast-1, single AZ, 2026-10-10 (UTC), up to 8 c5n.2xlarge, Amazon Linux 2023&lt;/li&gt;
&lt;li&gt;Mount: each client mounts S3 Files on its own (one efs-proxy per client)&lt;/li&gt;
&lt;li&gt;Load: VDBENCH, 1 MiB sequential read, &lt;code&gt;o_direct&lt;/code&gt;, fixed at 64 threads per client (only the client count changes)&lt;/li&gt;
&lt;li&gt;Simultaneous start: shared epoch. All clients wait until the same UNIX time (90 seconds after launch) and start measuring together. 60 seconds of warmup, then 180 seconds measured&lt;/li&gt;
&lt;li&gt;Range read: two patterns, same (all clients read the same 600 GiB file) and disjoint (each client reads a non-overlapping range)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Results: all clients read the same file
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Clients&lt;/th&gt;
&lt;th&gt;Aggregate&lt;/th&gt;
&lt;th&gt;Per client&lt;/th&gt;
&lt;th&gt;Ratio to 1 client&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;784.4 MB/s&lt;/td&gt;
&lt;td&gt;784.4 MB/s&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1,604.5 MB/s&lt;/td&gt;
&lt;td&gt;802.2 MB/s&lt;/td&gt;
&lt;td&gt;2.05x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3,148.4 MB/s&lt;/td&gt;
&lt;td&gt;787.1 MB/s&lt;/td&gt;
&lt;td&gt;4.01x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;6,282.6 MB/s&lt;/td&gt;
&lt;td&gt;785.3 MB/s&lt;/td&gt;
&lt;td&gt;8.01x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source of this table: &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md#L134-L139" rel="noopener noreferrer"&gt;Client-count measurement (same)&lt;/a&gt; (Japanese)&lt;/p&gt;

&lt;h3&gt;
  
  
  Results: each client reads a non-overlapping range
&lt;/h3&gt;

&lt;p&gt;I measured disjoint at 8 clients only. The aggregate was 7,210.0 MB/s, and per client 901.3 MB/s (a spread of 770 to 1,332), higher than the 8-client same result of 6,282.6 MB/s. Each client reads different data, so sharing and fanning out one read cannot explain the number. I did not measure disjoint at 1, 2, or 4 clients, so the shape of disjoint scaling is unconfirmed.&lt;/p&gt;

&lt;p&gt;Source of this table: &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md#L143-L147" rel="noopener noreferrer"&gt;Client-count measurement (disjoint)&lt;/a&gt; (Japanese)&lt;/p&gt;

&lt;h3&gt;
  
  
  Measurement limit: client-side totals only
&lt;/h3&gt;

&lt;p&gt;The numbers are the sum of what the clients reported. I could not reconcile them with CloudWatch &lt;code&gt;DataReadBytes&lt;/code&gt;: on the day, metrics arrived sparsely and did not land in the measurement window. The record also has no spread across repeated runs for each client count.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FYoshiki0705%2Fs3-burst-on-ontap-files%2Fab4d651fe338e8e885f468b6c93d2116a6f6cc2a%2Fdocs%2F_assets%2Fimages%2Fs3burst-s3files-internals-en%402x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FYoshiki0705%2Fs3-burst-on-ontap-files%2Fab4d651fe338e8e885f468b6c93d2116a6f6cc2a%2Fdocs%2F_assets%2Fimages%2Fs3burst-s3files-internals-en%402x.png" alt="The internals of Amazon S3 Files. Each client has its own efs-proxy that connects to the mount target over TLS, and S3 Files uses two paths,&lt;br&gt;
high-performance storage and direct bucket reads. A single mount tops out at one efs-proxy core, and the aggregate grows in proportion to client count up to 8 clients" width="800" height="686"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1: The internals of S3 Files. Each client has its own efs-proxy. A single mount tops out at one efs-proxy core, while the aggregate grows in proportion to client count (measured up to 8). The figure says the same thing as the tables above.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For capacity planning up to 8 clients, multiply roughly 785 MB/s per client (785.3 MB/s at 8 clients; c5n.2xlarge, all clients reading the same file) by the client count. I have not measured 9 or more, so check the conditions in the chapter "What I have not measured" before relying on that range.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing two ways to use the same data through two entry points: where the source of truth sits
&lt;/h2&gt;

&lt;p&gt;Both S3 Files and FSx for ONTAP with S3 Access Points let you use the same data through a file mount and the S3 API. What differs is the source of truth: an S3 bucket for S3 Files, an ONTAP volume for FSx for ONTAP. That difference bears on the speed of propagation, how throughput grows as you add clients, and how the bill comes out. The measured values do not share an environment, though, so this post does not isolate whether the differences come from where the source of truth sits.&lt;/p&gt;

&lt;p&gt;The two setups compared in this chapter are as follows.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;S3 Files: attach S3 Files to an S3 bucket and mount it over NFS from EC2. The S3 API reaches the bucket directly.&lt;/li&gt;
&lt;li&gt;FSx for ONTAP: use an ONTAP volume over NFS / SMB and attach S3 Access Points (the S3 API entry point attached to the volume) to the same volume. Data that does not fit on SSD can sit on the capacity pool, a tier with a lower storage price. Tiering All, one of the volume's tiering policies, places the data on the capacity pool.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The table below lines up the two setups by aspect. I wrote "not measured" where I did not measure. Measured values come from different environments, so each cell carries its environment.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;S3 Files (S3 bucket first)&lt;/th&gt;
&lt;th&gt;FSx for ONTAP + S3 Access Points (file system first)&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Source of truth&lt;/td&gt;
&lt;td&gt;The S3 bucket. Objects and files point at the same bucket&lt;/td&gt;
&lt;td&gt;The ONTAP volume&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-vs-flexcache.md#L144-L158" rel="noopener noreferrer"&gt;Design differences&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File-side entry&lt;/td&gt;
&lt;td&gt;NFSv4.1 / 4.2 only, through a per-client efs-proxy. nconnect is unsupported&lt;/td&gt;
&lt;td&gt;ONTAP serves NFS (v3, v4.0 to 4.2) and SMB directly&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/reference/comparison/finops-s3-vs-s3ap.md#L1165-L1169" rel="noopener noreferrer"&gt;Protocol comparison&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-quotas.html" rel="noopener noreferrer"&gt;S3 Files quotas&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 API-side entry&lt;/td&gt;
&lt;td&gt;The bucket itself&lt;/td&gt;
&lt;td&gt;An S3 Access Point attached to the volume&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-vs-flexcache.md#L144-L158" rel="noopener noreferrer"&gt;Design differences&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Propagation: write as a file, read with the S3 API&lt;/td&gt;
&lt;td&gt;p50 63,769.3 ms (a window of about 60 seconds). t3.small, NFSv4.2, 64 B, concurrency 1, n=30, 2026-09-01&lt;/td&gt;
&lt;td&gt;p50 44 ms. NFSv3, 128 MBps Single-AZ, 64 B, concurrency 1, n=30, 2026-08-09&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-measured.md#L42-L45" rel="noopener noreferrer"&gt;S3 Files measurement&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-vs-flexcache.md#L96-L101" rel="noopener noreferrer"&gt;FSx for ONTAP measurement&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Propagation: write with the S3 API, read as a file&lt;/td&gt;
&lt;td&gt;p50 1,533.1 ms (same environment as above)&lt;/td&gt;
&lt;td&gt;p50 3 ms. A direct NFS read of the Origin volume that received the write (it does not go through a FlexCache Cache volume; same environment as above)&lt;/td&gt;
&lt;td&gt;Same as above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conflict and locking&lt;/td&gt;
&lt;td&gt;When the same key is changed from the file side and the S3 API side, the S3 bucket side became the source of truth (3/3 converged on the S3 content). The file-side version was moved to &lt;code&gt;.s3files-lost+found-&amp;lt;fs-id&amp;gt;&lt;/code&gt;. Only conflicts inside the window (5 seconds later) were measured&lt;/td&gt;
&lt;td&gt;Not measured. I did not measure what happens when the same key is changed from the file side and the S3 API side, nor reconcile it with ONTAP-side file locking&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-measured.md#L146-L160" rel="noopener noreferrer"&gt;Conflict behavior&lt;/a&gt; (Japanese)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supported APIs, conditional writes, copy scope&lt;/td&gt;
&lt;td&gt;The S3 bucket API. I did not measure conditional writes or copy scope in this post&lt;/td&gt;
&lt;td&gt;Presigned URLs through S3 Access Points (&lt;code&gt;PutObject&lt;/code&gt; / &lt;code&gt;HeadObject&lt;/code&gt; / &lt;code&gt;GetObject&lt;/code&gt;) are verified (2026-08-19, n=30x4, succeeded under both SigV4 and SigV2). The official support table also lists &lt;code&gt;Presign&lt;/code&gt; as Supported (confirmed 2026-10-08, documented). Uploads are single &lt;code&gt;PutObject&lt;/code&gt; 5 GiB and whole object 50 GiB, with the limit judged at &lt;code&gt;CompleteMultipartUpload&lt;/code&gt;. The read side is asymmetric: a GET of 50 GiB + 1 byte succeeded (537 seconds, 2026-09-11)&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification-status.md" rel="noopener noreferrer"&gt;Verification status&lt;/a&gt; (Japanese)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Versioning / Object Lock / Lifecycle&lt;/td&gt;
&lt;td&gt;Bucket versioning is required. I did not measure Object Lock or Lifecycle in this post&lt;/td&gt;
&lt;td&gt;On the S3 Access Points side, S3 event notifications, lifecycle, and versioning are not supported (documented). ONTAP-side data protection (SnapLock and the like) is a separate mechanism; do not equate it with S3 Object Lock&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-vs-flexcache.md#L144-L158" rel="noopener noreferrer"&gt;Design differences&lt;/a&gt; (Japanese)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAM vs. file-side permissions&lt;/td&gt;
&lt;td&gt;An object written through the S3 API appears as &lt;code&gt;root:root&lt;/code&gt; on the file system (file 0644, directory 0755). A client mapped to non-root at the access point cannot write into a directory the S3 API created (reads go through). The official &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-synchronization.html" rel="noopener noreferrer"&gt;Synchronization&lt;/a&gt; page says POSIX permissions are stored in the S3 object's user-defined metadata, and that objects without metadata get default permissions. I have not found the specific values &lt;code&gt;root:root&lt;/code&gt; or 0644 / 0755 in the pages I read&lt;/td&gt;
&lt;td&gt;ONTAP NFS / SMB file permissions and the IAM on the S3 Access Points side are separate paths. I did not run a reconciling measurement in this post&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-measured.md#L130-L134" rel="noopener noreferrer"&gt;Owner of S3-API writes&lt;/a&gt; (Japanese)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where unread data sits&lt;/td&gt;
&lt;td&gt;The S3 bucket. Reads of 1 MiB or more go to the bucket even when the data is in high-performance storage&lt;/td&gt;
&lt;td&gt;SSD. Depending on the tiering policy, the capacity pool (Tiering All places everything there)&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-performance.html" rel="noopener noreferrer"&gt;Performance specifications&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/reference/comparison/finops-s3-vs-s3ap.md#L1050-L1055" rel="noopener noreferrer"&gt;Billing items table&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read billing items&lt;/td&gt;
&lt;td&gt;S3 GET plus metadata reads ($0.04 / GB; the model counts 4 KiB x operations). Small files held in high-performance storage also incur a data read charge ($0.04 / GB). For 4 MiB reads, CloudWatch &lt;code&gt;MetadataReadBytes&lt;/code&gt; registered about 6.0 times per object, with an average of about 4.1 KiB each (one observation, m5.large, 2026-10-16). The origin is unknown, and whether it reaches the bill is unconfirmed&lt;/td&gt;
&lt;td&gt;GET through S3 Access Points ($0.000029 / 1,000). Data on the capacity pool can incur a read request charge ($0.00037 / 1,000). Requests per GetObject: not confirmed (&lt;code&gt;CapacityPoolReadOperations&lt;/code&gt; was 0 in the measured window)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/reference/comparison/finops-s3-vs-s3ap.md#L1050-L1055" rel="noopener noreferrer"&gt;Billing items table&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency of reading data that is not held in the fast tier&lt;/td&gt;
&lt;td&gt;Not confirmed. 64 KiB p50 was 5.33 ms from high-performance storage and 4.05 ms straight from the bucket (c5n.9xlarge, n=30, one run, 2026-10-10). A separate t3.small record shows 10.8 ms vs. 55.0 ms (2026-09-01), which did not reproduce on c5n.9xlarge&lt;/td&gt;
&lt;td&gt;Not measured. There is one observation of +5.3 to +15.4 ms against the SSD side, but the reads did not reach the capacity pool, so it cannot be read as that delay&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md#L175-L179" rel="noopener noreferrer"&gt;Three reads&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md#L243-L247" rel="noopener noreferrer"&gt;Reads by tier&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adding clients: file path&lt;/td&gt;
&lt;td&gt;1 to 8 clients: 784.4 to 6,282.6 MB/s (8.01x). c5n.2xlarge, all clients read the same file, 2026-10-10. 9 or more: not measured&lt;/td&gt;
&lt;td&gt;1 to 8 clients: 2,664.58 to 11,916.29 MB/s (4.47x, all clients read the same file). With non-overlapping regions it does not grow, from 2,299.05 MB/s at 2 clients to 2,173.37 MB/s at 8. c5n.2xlarge, nconnect=16, second generation, 2,048 MBps, 2026-09-05&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md#L134-L139" rel="noopener noreferrer"&gt;S3 Files client count&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/perf-matrix-results.md#L421-L443" rel="noopener noreferrer"&gt;FSx for ONTAP client count&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adding clients: S3 API path&lt;/td&gt;
&lt;td&gt;The S3 API reaches the bucket directly. Reads from Amazon S3 went from 495.4 to 2,030.5 MB/s at 1 to 4 clients (4.10x, c5n.2xlarge, 2026-09-01 to 02). Not measured on a bucket with S3 Files attached&lt;/td&gt;
&lt;td&gt;At 1 to 4 clients, writes went 129.4 to 131.0 MB/s (1.01x) and reads 480.9 to 612.3 MB/s (1.27x). 128 MBps configuration, nearly flat from 2 clients. Clients share the purchased throughput capacity&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/throughput-iops-concurrency.md#L784-L789" rel="noopener noreferrer"&gt;Measurement record of the previous post&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Published limits&lt;/td&gt;
&lt;td&gt;25,000 connections per file system, 3 GiB/s read per client. Aggregate read is "up to terabytes per second" in AWS's wording&lt;/td&gt;
&lt;td&gt;You buy throughput capacity. The minimum for Single-AZ first generation is 128 MBps&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-quotas.html" rel="noopener noreferrer"&gt;quotas&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-performance.html" rel="noopener noreferrer"&gt;Performance specifications&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/reference/comparison/finops-s3-vs-s3ap.md#L374-L376" rel="noopener noreferrer"&gt;Fixed-cost floor&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed cost&lt;/td&gt;
&lt;td&gt;None. S3 storage plus what you use&lt;/td&gt;
&lt;td&gt;Floor of $269.57 a month for Single-AZ first generation (SSD 1,024 GiB + 128 MBps, prices read 2026-08-09)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/reference/comparison/finops-s3-vs-s3ap.md#L374-L376" rel="noopener noreferrer"&gt;Fixed-cost floor&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prerequisites and constraints&lt;/td&gt;
&lt;td&gt;Bucket versioning required. Clients are Linux on AWS. NFSv4.1 / 4.2 only. About 60 seconds from file to bucket&lt;/td&gt;
&lt;td&gt;On the S3 Access Points side, S3 event notifications, lifecycle, and versioning are not supported. Capacity pool read charges. Clients share the throughput capacity&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-vs-flexcache.md#L144-L158" rel="noopener noreferrer"&gt;Design differences&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-performance.html" rel="noopener noreferrer"&gt;Performance specifications&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The propagation rows are not a same-conditions comparison, because the NFS version, client, and configuration differ. The client-count rows differ too: S3 Files used VDBENCH with 64 threads per client, and FSx for ONTAP used a second-generation 2,048 MBps configuration with nconnect=16. I read in full on 2026-10-16 the two AWS documents this chapter relies on: the S3 User Guide pages &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-performance.html" rel="noopener noreferrer"&gt;Performance specifications&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-synchronization.html" rel="noopener noreferrer"&gt;Understanding how synchronization works&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Source of this table: &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-vs-flexcache.md#L144-L158" rel="noopener noreferrer"&gt;Design differences table&lt;/a&gt;. For the measured rows, the sources are the links in the "Source" column.&lt;/p&gt;

&lt;h3&gt;
  
  
  The four stages of file-to-bucket propagation, and how to treat the maximum reflection time
&lt;/h3&gt;

&lt;p&gt;The "window of about 60 seconds" in the propagation rows is not one wait but four stages. The measured p50 of 63,769.3 ms is the whole span from (a) to (d).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Definition&lt;/th&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;(a) File write complete and persisted&lt;/td&gt;
&lt;td&gt;The moment the write to NFS finishes through efs-proxy. Separate from propagation to S3&lt;/td&gt;
&lt;td&gt;NFS semantics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(b) No-write wait&lt;/td&gt;
&lt;td&gt;The wait counted from when writes to the same file stop. The official Synchronization page states the "60 seconds with no writes" condition&lt;/td&gt;
&lt;td&gt;Documented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(c) Export start&lt;/td&gt;
&lt;td&gt;The moment a file that satisfies the wait condition becomes an export target. The official Performance page states the roughly 60-second aggregation&lt;/td&gt;
&lt;td&gt;Documented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(d) Visible through the S3 API&lt;/td&gt;
&lt;td&gt;After the export completes. The measured p50 of 63,769.3 ms is this whole (a) to (d)&lt;/td&gt;
&lt;td&gt;Measured (t3.small, 64 B, concurrency 1, n=30, 2026-09-01)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The export side agrees across two sources. Against the client-side max of 66.33 seconds, the CloudWatch &lt;code&gt;ExportAge&lt;/code&gt; peak was 66.06 seconds, a 0.27-second difference (all 30 runs fell in 63.8 to 66.3 seconds).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The maximum reflection time cannot be used as a guaranteed value.&lt;/strong&gt; On the import side (S3 to file), the client-side max of 14.36 seconds and the CloudWatch &lt;code&gt;ImportAge&lt;/code&gt; peak of 62.80 seconds do not agree. I could not find a maximum reflection time in the official documentation, so I do not treat it as an upper bound, and I write the representative value as the client-side p50. I do not write "reflected within 60 seconds", "reflected every minute even under continuous updates", or "never synchronized" either, because none has grounds. This explanatory model (wait, then aggregate, then export) is not an SLA.&lt;/p&gt;

&lt;p&gt;Three notes follow.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In a conflict (the same key changed from the file side and the S3 API side), the S3 bucket side became the source of truth (3/3 converged on the S3 content). The file-side version was moved to &lt;code&gt;.s3files-lost+found-&amp;lt;fs-id&amp;gt;&lt;/code&gt; (matching the CloudWatch &lt;code&gt;LostAndFoundFiles&lt;/code&gt; peak of 3.00).&lt;/li&gt;
&lt;li&gt;That directory is not created on conflict; it is present from the start (&lt;code&gt;0700 root:root&lt;/code&gt;), and what happens on conflict is the eviction event. The official Synchronization page says files in this directory are not copied to the S3 bucket (I did not measure this).&lt;/li&gt;
&lt;li&gt;For large rename / move operations, a rename on the file side finishes immediately, while on the S3 side it becomes a per-object copy and delete. AWS says a rename of 100,000 files takes several minutes to reflect on the S3 side, and that the prefixes from before and after the rename are both visible midway. I have not measured this behavior separately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Measurements that separate the propagation conditions (long continuous appends, after updates stop, while another file is being updated, concurrent updates) are deferred to &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/ab4d651fe338e8e885f468b6c93d2116a6f6cc2a/docs/ja/verification/s3files-sync-additional-plan.md" rel="noopener noreferrer"&gt;a separate verification plan&lt;/a&gt; (a plan, not a new measurement).&lt;/p&gt;

&lt;p&gt;What this comparison can settle is two things: whether the source of truth lives in an S3 bucket or an ONTAP volume, and whether you can accept the roughly 60 seconds from file to bucket. I estimate the monthly cost in the next chapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monthly cost estimate: an order that splits by object size, and room for assumptions to move it
&lt;/h2&gt;

&lt;p&gt;I apply the same list prices to the two setups from the previous chapter, with the stored volume fixed at 20 TiB. I estimated 10 cells: 5 patterns that differ in monthly read and write volume (P1, P4, P5, P6, P8) times 2 object sizes (64 KiB and 4 MiB). The amounts in this chapter are list price x quantity for ap-northeast-1, with prices read on 2026-08-09, and they are subtotals of the main line items. The subtotals add up the line items shown in the tables, and some items are not included. They are not measurements and not invoices. "Base assumptions" below means the values in the common-assumptions table in the next section, "Five patterns".&lt;/p&gt;

&lt;p&gt;Under this model's base assumptions, in all 10 cells, &lt;strong&gt;the subtotal is lower on FSx for ONTAP at 64 KiB and lower via S3 Files at 4 MiB&lt;/strong&gt;. At 64 KiB, FSx for ONTAP comes to $1,783.11 to $2,218.13 a month and S3 Files to $7,928.32 to $12,030.81. At 4 MiB, S3 Files comes to $512.14 to $543.52 a month and FSx for ONTAP to $1,781.28 to $1,788.08. Because the order was the same in all five patterns, what splits the order in this model is the object size, not the read and write volume. The candidate for the dividing line is the S3 Files import threshold (default 128 KiB). I estimated two sizes, 64 KiB and 4 MiB, so the tables have no rows near 128 KiB or at 1 MiB. The reading that the dividing line sits at the threshold comes from those two points and from how the price works (high-performance storage is charged only for sizes below the threshold), and I did not estimate sizes at the boundary. The import condition is &lt;code&gt;sizeLessThan&lt;/code&gt;, so I treat exactly 128 KiB as not imported. This order is a result under the base assumptions, and it can flip when they change, so check the section "Assumptions under which the order can flip" in this chapter against your own values.&lt;/p&gt;

&lt;h3&gt;
  
  
  Five patterns: total monthly reads and writes
&lt;/h3&gt;

&lt;p&gt;Writing "read N times a month" alone does not fix the total volume read. Whether the N reads cover all objects or only some of them changes the total several times over. I define each pattern with three numbers: the working-set ratio, the number of reads per working-set object, and the write volume. The table shows the monthly read and write volumes derived from them. The working set is the part that is read within the month. The working-set ratio is the working set's share of the stored volume, and the number of reads per working-set object is how many times each of its objects is read in a month. I set the write volume as a share of the stored volume (the churn rate).&lt;/p&gt;

&lt;p&gt;Take P4 as an example. 10% of the 20 TiB stored (2 TiB) is the working set, and each of its objects is read 10 times a month. The volume read is 2 TiB x 10 = 20 TiB, which is 1.0x the stored volume. The other 90% (18 TiB) is not read at all within the month. With 4 MiB objects, that is 524,288 objects read 10 times each, or 5,242,880 reads. P4 has no writes, so the stored volume stays at 20 TiB.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Working-set ratio&lt;/th&gt;
&lt;th&gt;Reads per working-set object&lt;/th&gt;
&lt;th&gt;Monthly read volume (multiple of stored volume)&lt;/th&gt;
&lt;th&gt;Monthly write volume (multiple of stored volume)&lt;/th&gt;
&lt;th&gt;Basis of the values&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;P1 Cold archive&lt;/td&gt;
&lt;td&gt;5% (1 TiB)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1 TiB (0.05x)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Placeholder (no source)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P4 Re-read a small working set heavily&lt;/td&gt;
&lt;td&gt;10% (2 TiB)&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;20 TiB (1.0x)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;This model's estimate assumption (not a measurement)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P5 Read-mostly with light ingest&lt;/td&gt;
&lt;td&gt;10% (2 TiB)&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;20 TiB (1.0x)&lt;/td&gt;
&lt;td&gt;1 TiB (0.05x, churn rate 5%)&lt;/td&gt;
&lt;td&gt;Reads as in P4. The write volume is a placeholder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P6 Mixed read and write&lt;/td&gt;
&lt;td&gt;50% (10 TiB)&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;40 TiB (2.0x)&lt;/td&gt;
&lt;td&gt;20 TiB (1.0x)&lt;/td&gt;
&lt;td&gt;Placeholder (no source)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P8 Write-centred&lt;/td&gt;
&lt;td&gt;10% (2 TiB)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2 TiB (0.1x)&lt;/td&gt;
&lt;td&gt;20 TiB (1.0x)&lt;/td&gt;
&lt;td&gt;Placeholder (no source)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source of this table: &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/84a283c5c054c8205ff6e929bec94c56d0daeab7/docs/en/reference/comparison/finops-s3-vs-s3ap.md#the-patterns" rel="noopener noreferrer"&gt;The patterns&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The placeholder values are numbers I put in to make the arithmetic work. They are not taken from a real workload. Replace them with your own monthly read and write volumes. The cost model also has P2, P3, and P7, but I left them out of the figures and tables because they have the same monthly read and write volumes, and so the same amounts, as P4 and P6 (P4 matches P2 and P3, and P6 matches P7).&lt;/p&gt;

&lt;p&gt;The following assumptions are common to all 10 cells.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Region&lt;/td&gt;
&lt;td&gt;ap-northeast-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prices&lt;/td&gt;
&lt;td&gt;List prices read on 2026-08-09 (on-demand, excluding tax). I re-read the 2026-08-09 record with the Price List API (read-only) on 2026-10-16, and the values were the same. It covers 10 items and does not include the two PUT request prices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stored volume&lt;/td&gt;
&lt;td&gt;20,480 GiB (20 TiB). Common to all cells&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FSx for ONTAP&lt;/td&gt;
&lt;td&gt;Single-AZ first generation, 128 MBps. Storage efficiency 40% on the SSD tier and 20% on the capacity pool tier. 30% of the data on the capacity pool (common to all patterns). Reads are served from the file system, and writes are charged as PUT through S3 Access Points&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Files&lt;/td&gt;
&lt;td&gt;Import threshold 128 KiB. For sizes below the threshold, the whole stored volume is assumed to be held as active in high-performance storage. Expiry is the default 30 days&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source of this table: &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/84a283c5c054c8205ff6e929bec94c56d0daeab7/docs/en/reference/comparison/finops-s3-vs-s3ap.md#a-common-baseline--20-tib-under-the-same-terms" rel="noopener noreferrer"&gt;Common baseline&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost figure 1: monthly cost of P1 and P4, which have no writes
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdww956jdjlu1izzjur16.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdww956jdjlu1izzjur16.png" alt="Monthly cost breakdown for P1 and P4, which have no writes. For each of P1 and P4, and for two sizes, 64 KiB and 4 MiB, three bars are shown side by side: reading directly with the S3 API, via S3 Files, and FSx for ONTAP. At 64 KiB, via S3 Files is $7,928.32 and $8,755.20, and FSx for ONTAP is $1,783.11 and $1,818.50. At 4 MiB, via S3 Files is $512.14 and $514.74, and FSx for ONTAP is $1,781.28 and $1,781.83. The horizontal axis runs from 0 to $12,500 and is shared with Figure 2" width="800" height="1537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cost figure 1: breakdown of the monthly cost of P1 and P4, which have no writes (ap-northeast-1, list price x quantity, not an invoice). The horizontal axis is shared with cost figure 2. The working-set ratio and read count of P1 are placeholders with no source. "Figure 1" and "Figure 2" inside the images mean these two cost figures, not the internals figure in a preceding chapter. The figure shows the same numbers as the table below (&lt;a href="https://raw.githubusercontent.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/84a283c5c054c8205ff6e929bec94c56d0daeab7/docs/_assets/images/s3burst-s3files-cost-structure-en-dark@2x.png" rel="noopener noreferrer"&gt;dark theme version&lt;/a&gt;).&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern / size&lt;/th&gt;
&lt;th&gt;Monthly operations&lt;/th&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;th&gt;S3 Files high-performance storage&lt;/th&gt;
&lt;th&gt;Fixed bandwidth&lt;/th&gt;
&lt;th&gt;Requests etc.&lt;/th&gt;
&lt;th&gt;Of which S3 Files metadata charges&lt;/th&gt;
&lt;th&gt;Main-item subtotal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;P1 / 64 KiB&lt;/td&gt;
&lt;td&gt;16,777,216 reads, no writes&lt;/td&gt;
&lt;td&gt;Direct read with the S3 API&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$6.21&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$518.21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P1 / 64 KiB&lt;/td&gt;
&lt;td&gt;16,777,216 reads, no writes&lt;/td&gt;
&lt;td&gt;Via S3 Files&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;$7,372.80&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$43.52&lt;/td&gt;
&lt;td&gt;$2.56&lt;/td&gt;
&lt;td&gt;$7,928.32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P1 / 64 KiB&lt;/td&gt;
&lt;td&gt;16,777,216 reads, no writes&lt;/td&gt;
&lt;td&gt;FSx for ONTAP&lt;/td&gt;
&lt;td&gt;$1,665.28&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$115.97&lt;/td&gt;
&lt;td&gt;$1.86&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$1,783.11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P1 / 4 MiB&lt;/td&gt;
&lt;td&gt;262,144 reads, no writes&lt;/td&gt;
&lt;td&gt;Direct read with the S3 API&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$0.10&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$512.10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P1 / 4 MiB&lt;/td&gt;
&lt;td&gt;262,144 reads, no writes&lt;/td&gt;
&lt;td&gt;Via S3 Files&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$0.14&lt;/td&gt;
&lt;td&gt;$0.04&lt;/td&gt;
&lt;td&gt;$512.14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P1 / 4 MiB&lt;/td&gt;
&lt;td&gt;262,144 reads, no writes&lt;/td&gt;
&lt;td&gt;FSx for ONTAP&lt;/td&gt;
&lt;td&gt;$1,665.28&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$115.97&lt;/td&gt;
&lt;td&gt;$0.03&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$1,781.28&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P4 / 64 KiB&lt;/td&gt;
&lt;td&gt;335,544,320 reads, no writes&lt;/td&gt;
&lt;td&gt;Direct read with the S3 API&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$124.15&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$636.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P4 / 64 KiB&lt;/td&gt;
&lt;td&gt;335,544,320 reads, no writes&lt;/td&gt;
&lt;td&gt;Via S3 Files&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;$7,372.80&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$870.40&lt;/td&gt;
&lt;td&gt;$51.20&lt;/td&gt;
&lt;td&gt;$8,755.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P4 / 64 KiB&lt;/td&gt;
&lt;td&gt;335,544,320 reads, no writes&lt;/td&gt;
&lt;td&gt;FSx for ONTAP&lt;/td&gt;
&lt;td&gt;$1,665.28&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$115.97&lt;/td&gt;
&lt;td&gt;$37.25&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$1,818.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P4 / 4 MiB&lt;/td&gt;
&lt;td&gt;5,242,880 reads, no writes&lt;/td&gt;
&lt;td&gt;Direct read with the S3 API&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$1.94&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$513.94&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P4 / 4 MiB&lt;/td&gt;
&lt;td&gt;5,242,880 reads, no writes&lt;/td&gt;
&lt;td&gt;Via S3 Files&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$2.74&lt;/td&gt;
&lt;td&gt;$0.80&lt;/td&gt;
&lt;td&gt;$514.74&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P4 / 4 MiB&lt;/td&gt;
&lt;td&gt;5,242,880 reads, no writes&lt;/td&gt;
&lt;td&gt;FSx for ONTAP&lt;/td&gt;
&lt;td&gt;$1,665.28&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$115.97&lt;/td&gt;
&lt;td&gt;$0.58&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$1,781.83&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source of this table: &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/84a283c5c054c8205ff6e929bec94c56d0daeab7/docs/en/reference/comparison/finops-s3-vs-s3ap.md#pattern-p1--64-kib-objects" rel="noopener noreferrer"&gt;P1 / 64 KiB&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/84a283c5c054c8205ff6e929bec94c56d0daeab7/docs/en/reference/comparison/finops-s3-vs-s3ap.md#pattern-p1--4-mib-objects" rel="noopener noreferrer"&gt;P1 / 4 MiB&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/84a283c5c054c8205ff6e929bec94c56d0daeab7/docs/en/reference/comparison/finops-s3-vs-s3ap.md#pattern-p4--64-kib-objects" rel="noopener noreferrer"&gt;P4 / 64 KiB&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/84a283c5c054c8205ff6e929bec94c56d0daeab7/docs/en/reference/comparison/finops-s3-vs-s3ap.md#pattern-p4--4-mib-objects" rel="noopener noreferrer"&gt;P4 / 4 MiB&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;"Requests etc." is the sum of S3 requests, S3 Files read and write charges (metadata charges included), and FSx for ONTAP capacity pool read requests and PUT. It is the purple category in the figure. "Storage" for FSx for ONTAP is SSD and capacity pool storage added together.&lt;/p&gt;

&lt;p&gt;The FSx for ONTAP rows assume reads are served from the file system, and they do not include read charges through S3 Access Points. The direct-read rows for the S3 API appear only for P1 and P4, which have no writes. In the patterns with writes, the write path differs by setup (S3 API PUT, NFS write plus sync, PUT through S3 Access Points), and putting them in one table would mix the write assumptions.&lt;/p&gt;

&lt;p&gt;In the 4 MiB rows, the S3 Files subtotal is nearly the same as the direct S3 API read subtotal ($512.14 vs. $512.10 for P1, and $514.74 vs. $513.94 for P4). 4 MiB is at or above the threshold, so the data is not held in high-performance storage and is read straight from the bucket. In the 64 KiB rows, S3 Files adds $7,372.80 of high-performance storage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost figure 2: monthly cost of P5, P6, and P8, which have writes
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3a3zgheirerb0c4vj7ne.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3a3zgheirerb0c4vj7ne.png" alt="Monthly cost breakdown for P5, P6, and P8, which have writes. For two sizes, 64 KiB and 4 MiB, two bars are shown side by side: via S3 Files and FSx for ONTAP. At 64 KiB, via S3 Files is $8,875.46, $12,030.81, and $11,114.33, and FSx for ONTAP is $1,836.61, $2,218.13, and $2,147.36. At 4 MiB, via S3 Files is $516.04, $543.52, and $538.32, and FSx for ONTAP is $1,782.11, $1,788.08, and $1,786.97. The horizontal axis runs from 0 to $12,500 and is shared with Figure 1" width="800" height="1667"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cost figure 2: breakdown of the monthly cost of P5, P6, and P8, which have writes (ap-northeast-1, list price x quantity, not an invoice). The horizontal axis is shared with cost figure 1. The write volume of P5 and the working-set ratio, read count, and write volume of P6 and P8 are placeholders with no source. The figure shows the same numbers as the table below (&lt;a href="https://raw.githubusercontent.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/84a283c5c054c8205ff6e929bec94c56d0daeab7/docs/_assets/images/s3burst-s3files-cost-structure-writes-en-dark@2x.png" rel="noopener noreferrer"&gt;dark theme version&lt;/a&gt;).&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern / size&lt;/th&gt;
&lt;th&gt;Monthly operations&lt;/th&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;th&gt;S3 Files high-performance storage&lt;/th&gt;
&lt;th&gt;Fixed bandwidth&lt;/th&gt;
&lt;th&gt;Requests etc.&lt;/th&gt;
&lt;th&gt;Of which S3 Files metadata charges&lt;/th&gt;
&lt;th&gt;Main-item subtotal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;P5 / 64 KiB&lt;/td&gt;
&lt;td&gt;335,544,320 reads, 16,777,216 objects written&lt;/td&gt;
&lt;td&gt;Via S3 Files&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;$7,372.80&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$990.66&lt;/td&gt;
&lt;td&gt;$55.68&lt;/td&gt;
&lt;td&gt;$8,875.46&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P5 / 64 KiB&lt;/td&gt;
&lt;td&gt;335,544,320 reads, 16,777,216 objects written&lt;/td&gt;
&lt;td&gt;FSx for ONTAP&lt;/td&gt;
&lt;td&gt;$1,665.28&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$115.97&lt;/td&gt;
&lt;td&gt;$55.36&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$1,836.61&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P5 / 4 MiB&lt;/td&gt;
&lt;td&gt;5,242,880 reads, 262,144 objects written&lt;/td&gt;
&lt;td&gt;Via S3 Files&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$4.04&lt;/td&gt;
&lt;td&gt;$0.87&lt;/td&gt;
&lt;td&gt;$516.04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P5 / 4 MiB&lt;/td&gt;
&lt;td&gt;5,242,880 reads, 262,144 objects written&lt;/td&gt;
&lt;td&gt;FSx for ONTAP&lt;/td&gt;
&lt;td&gt;$1,665.28&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$115.97&lt;/td&gt;
&lt;td&gt;$0.87&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$1,782.11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P6 / 64 KiB&lt;/td&gt;
&lt;td&gt;671,088,640 reads, 335,544,320 objects written&lt;/td&gt;
&lt;td&gt;Via S3 Files&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;$7,372.80&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$4,146.01&lt;/td&gt;
&lt;td&gt;$192.00&lt;/td&gt;
&lt;td&gt;$12,030.81&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P6 / 64 KiB&lt;/td&gt;
&lt;td&gt;671,088,640 reads, 335,544,320 objects written&lt;/td&gt;
&lt;td&gt;FSx for ONTAP&lt;/td&gt;
&lt;td&gt;$1,665.28&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$115.97&lt;/td&gt;
&lt;td&gt;$436.88&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$2,218.13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P6 / 4 MiB&lt;/td&gt;
&lt;td&gt;10,485,760 reads, 5,242,880 objects written&lt;/td&gt;
&lt;td&gt;Via S3 Files&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$31.52&lt;/td&gt;
&lt;td&gt;$3.00&lt;/td&gt;
&lt;td&gt;$543.52&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P6 / 4 MiB&lt;/td&gt;
&lt;td&gt;10,485,760 reads, 5,242,880 objects written&lt;/td&gt;
&lt;td&gt;FSx for ONTAP&lt;/td&gt;
&lt;td&gt;$1,665.28&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$115.97&lt;/td&gt;
&lt;td&gt;$6.83&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$1,788.08&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P8 / 64 KiB&lt;/td&gt;
&lt;td&gt;33,554,432 reads, 335,544,320 objects written&lt;/td&gt;
&lt;td&gt;Via S3 Files&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;$7,372.80&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$3,229.53&lt;/td&gt;
&lt;td&gt;$94.72&lt;/td&gt;
&lt;td&gt;$11,114.33&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P8 / 64 KiB&lt;/td&gt;
&lt;td&gt;33,554,432 reads, 335,544,320 objects written&lt;/td&gt;
&lt;td&gt;FSx for ONTAP&lt;/td&gt;
&lt;td&gt;$1,665.28&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$115.97&lt;/td&gt;
&lt;td&gt;$366.11&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$2,147.36&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P8 / 4 MiB&lt;/td&gt;
&lt;td&gt;524,288 reads, 5,242,880 objects written&lt;/td&gt;
&lt;td&gt;Via S3 Files&lt;/td&gt;
&lt;td&gt;$512.00&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$26.32&lt;/td&gt;
&lt;td&gt;$1.48&lt;/td&gt;
&lt;td&gt;$538.32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P8 / 4 MiB&lt;/td&gt;
&lt;td&gt;524,288 reads, 5,242,880 objects written&lt;/td&gt;
&lt;td&gt;FSx for ONTAP&lt;/td&gt;
&lt;td&gt;$1,665.28&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$115.97&lt;/td&gt;
&lt;td&gt;$5.72&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;$1,786.97&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source of this table: &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/84a283c5c054c8205ff6e929bec94c56d0daeab7/docs/en/reference/comparison/finops-s3-vs-s3ap.md#pattern-p5--64-kib-objects" rel="noopener noreferrer"&gt;P5 / 64 KiB&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/84a283c5c054c8205ff6e929bec94c56d0daeab7/docs/en/reference/comparison/finops-s3-vs-s3ap.md#pattern-p5--4-mib-objects" rel="noopener noreferrer"&gt;P5 / 4 MiB&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/84a283c5c054c8205ff6e929bec94c56d0daeab7/docs/en/reference/comparison/finops-s3-vs-s3ap.md#pattern-p6--64-kib-objects" rel="noopener noreferrer"&gt;P6 / 64 KiB&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/84a283c5c054c8205ff6e929bec94c56d0daeab7/docs/en/reference/comparison/finops-s3-vs-s3ap.md#pattern-p6--4-mib-objects" rel="noopener noreferrer"&gt;P6 / 4 MiB&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/84a283c5c054c8205ff6e929bec94c56d0daeab7/docs/en/reference/comparison/finops-s3-vs-s3ap.md#pattern-p8--64-kib-objects" rel="noopener noreferrer"&gt;P8 / 64 KiB&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/84a283c5c054c8205ff6e929bec94c56d0daeab7/docs/en/reference/comparison/finops-s3-vs-s3ap.md#pattern-p8--4-mib-objects" rel="noopener noreferrer"&gt;P8 / 4 MiB&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Adding writes adds, on the S3 Files side, PUT to the S3 bucket and, at 64 KiB, import writes to high-performance storage. On the FSx for ONTAP side it adds PUT through S3 Access Points. For P6 at 64 KiB, "Requests etc." is $4,146.01 via S3 Files and $436.88 on FSx for ONTAP. Across the three patterns of cost figure 2, the 4 MiB subtotals stay within $516.04 to $543.52 via S3 Files and $1,782.11 to $1,788.08 on FSx for ONTAP.&lt;/p&gt;

&lt;h3&gt;
  
  
  Items that push up the S3 Files subtotal at 64 KiB: high-performance storage and the number of writes
&lt;/h3&gt;

&lt;p&gt;S3 Files imports files below the threshold (default 128 KiB) into high-performance storage and charges $0.36 / GB-month for the amount held. Files at or above the threshold are not held and are read straight from the bucket. The model assumes the whole stored volume (20,480 GiB) is active at the small size. That makes high-performance storage $7,372.80 in all five 64 KiB cells and $0.00 in all five 4 MiB cells.&lt;/p&gt;

&lt;p&gt;On the FSx for ONTAP side, storage of $1,665.28 and fixed bandwidth of $115.97 are the same in all 10 cells. Only the "Requests etc." column moves with size and with read and write volume.&lt;/p&gt;

&lt;p&gt;Why the S3 Files subtotal is higher at 64 KiB splits into two cases by pattern. In P1, P4, and P5, the S3 Files subtotal falls below FSx for ONTAP once the $7,372.80 of high-performance storage is removed. In P6 and P8 it stays higher even after the removal. Those two write 335,544,320 objects, and the S3 bucket PUT price ($0.0047 / 1,000) is higher than the PUT price through S3 Access Points ($0.00108 / 1,000), which also contributes. Those two PUT prices are still the values read on 2026-08-09 and were not part of the 2026-10-16 re-read.&lt;/p&gt;

&lt;p&gt;The high-performance storage amount follows from assuming a 100% active share. It is not a measurement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Metadata charges: the accounting assumption and one observation
&lt;/h3&gt;

&lt;p&gt;The model counts S3 Files metadata as 4 KiB x operations, in separate import-side and read-side lines, and includes both in the subtotal. The column "Of which S3 Files metadata charges" in the tables is the sum of those two lines. Whether metadata charges also apply to reads of 1 MiB or larger is unconfirmed, because the wording of AWS's metering page and pricing page does not agree. I left them in, so if they do not apply, the subtotal falls by the amount in that column.&lt;/p&gt;

&lt;p&gt;I have one observation (2026-10-16, ap-northeast-1, m5.large, not reproduced). I read 1,000 4 MiB objects once each with &lt;code&gt;dd&lt;/code&gt;, and CloudWatch &lt;code&gt;MetadataReadBytes&lt;/code&gt; registered about 6.0 times per object, with an average of about 4.1 KiB each. Changing the I/O size to 1 MiB or 4 MiB made no difference. The 64 KiB control with the same procedure registered about 2.0 times. I do not know where the roughly 6 comes from, so I have not applied that count to the "4 KiB x operations" count in the model. &lt;code&gt;DataReadBytes&lt;/code&gt; for 4 MiB had 0 data points, so I could not confirm whether the reads went through NFS or directly to the bucket. For the ingest of 4 MiB objects written through the S3 API, &lt;code&gt;MetadataWriteBytes&lt;/code&gt; had 0 data points. I cannot tell whether no ingest happened or whether it is not metered.&lt;/p&gt;

&lt;p&gt;What registered is a CloudWatch metric. I have not confirmed whether it is the same as the billing meter or whether this count reaches the bill. Cost Explorer right after the measurement showed no usage. That is within the reflection delay, so it is not evidence that nothing is billed. I plan to re-check in Cost Explorer after a few days. I did not measure other instance types, nconnect, multiple processes, or other object sizes. Until I can confirm, the metadata column in the tables keeps the assumption of one operation per read, and it may not match the actual bill.&lt;/p&gt;

&lt;h3&gt;
  
  
  Assumptions under which the order can flip: what to replace with your values
&lt;/h3&gt;

&lt;p&gt;The order in this chapter is a result under fixed assumptions. If you replace the following assumptions with values from your environment, the amounts and the order move. This post does not state at which values the order flips.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Share of data on the capacity pool: fixed at 30% in every pattern.&lt;/li&gt;
&lt;li&gt;Storage efficiency of the SSD tier: fixed at 40%.&lt;/li&gt;
&lt;li&gt;Active share of S3 Files high-performance storage: fixed at 100%.&lt;/li&gt;
&lt;li&gt;S3 Files import threshold: the default 128 KiB.&lt;/li&gt;
&lt;li&gt;FSx for ONTAP deployment: Single-AZ first generation, 128 MBps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The capacity pool share and the active share of high-performance storage are fixed values decoupled from the working-set ratio. In P1, P4, and P5 the data touched within the month (the working set plus writes) does not reach the whole stored volume, so the two shares can be set again to match the volume actually touched. Fitting the active share of high-performance storage would lower the S3 Files subtotal at 64 KiB. For the FSx for ONTAP subtotal when the capacity pool share is fitted, the generated document only says that the two together would go down, and I have not checked each one separately. In P6 and P8 the data touched within the month adds up to the stored volume or more, so if the capacity pool share is assumed to cover the whole volume, the FSx for ONTAP subtotal would rise. The directions come from the &lt;a href="https://github.com/Yoshiki0705/S3-Burst-on-ONTAP-Files/blob/84a283c5c054c8205ff6e929bec94c56d0daeab7/docs/en/reference/comparison/finops-s3-vs-s3ap.md#a-common-baseline--20-tib-under-the-same-terms" rel="noopener noreferrer"&gt;generated document&lt;/a&gt;, and I have not calculated the change in amounts. One more point: for S3 Files at 64 KiB in P1 and P4, which have no writes, the model counts no first-read stream (the GET from the bucket and the import write into high-performance storage) and counts all reads as high-performance storage reads. Counting it would add some items and remove others, and the model does not settle the net direction.&lt;/p&gt;

&lt;p&gt;The capacity pool read requests included in "Requests etc." for FSx for ONTAP assume one request per GET on the 30% of reads that hit the capacity pool. The actual count is unconfirmed.&lt;/p&gt;

&lt;p&gt;An observation that helps comes from an adjacent repository (the FSx for ONTAP monitoring integrations). On a file system of the same type (Single-AZ first generation, 128 MBps, SSD 1,024 GiB), on 2026-10-11, I read volumes that had been created with ALL and written to directly, from a Lambda function outside the VPC. CloudWatch &lt;code&gt;CapacityPoolReadOperations&lt;/code&gt; came to 0 per object at 64 KiB, about 5.4 to 5.6 at 1 MiB, and about 29.8 to 33.2 at 5 MiB (source: &lt;a href="https://github.com/Yoshiki0705/FSx-for-ONTAP-Observability-integrations/blob/main/docs/ja/s3ap-throughput-benchmark.md#%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97%E3%81%94%E3%81%A8%E3%81%AE%E3%82%AD%E3%83%A3%E3%83%91%E3%82%B7%E3%83%86%E3%82%A3%E3%83%97%E3%83%BC%E3%83%AB%E8%AA%AD%E3%81%BF%E5%8F%96%E3%82%8A%E5%B0%8E%E5%87%BA%E5%80%A4" rel="noopener noreferrer"&gt;per-invocation reads&lt;/a&gt;, Japanese). The 1 MiB and 5 MiB values are for cases where operations registered, and 2 of the 8 runs built the same way registered none. The sizes measured were 64 KiB, 1 MiB, and 5 MiB; 4 MiB was not measured. What was counted is the CloudWatch operation count, not a billed count, and that record also leaves the relation between the two unconfirmed. The measurement date, the volume, and the client all differ from this post's measurements. For these reasons I did not apply these operation counts to the model's count (one request per GET). The capacity pool read request amount that follows from the count the model assumed is $0.03 to $1.16 across the five 4 MiB cells.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading data on the capacity pool: billing items exist, latency is unmeasured
&lt;/h3&gt;

&lt;p&gt;Reading an object on the capacity pool of a Tiering All volume through S3 Access Points can incur two billing items: GET through S3 Access Points and capacity pool read requests (documented). This post's measurements did not produce the number of requests per GetObject or the latency of reading from the capacity pool. The capacity pool storage price is lower than SSD, $0.0238 vs. $0.15 per GB-month (Single-AZ, ap-northeast-1, read 2026-08-09).&lt;/p&gt;

&lt;p&gt;These are the p50 observations of GET through S3 Access Points from a local machine.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Object size&lt;/th&gt;
&lt;th&gt;SSD side&lt;/th&gt;
&lt;th&gt;After changing to Tiering All&lt;/th&gt;
&lt;th&gt;p50 difference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;64 KiB&lt;/td&gt;
&lt;td&gt;46.5 ms&lt;/td&gt;
&lt;td&gt;56.4 ms&lt;/td&gt;
&lt;td&gt;+9.9 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 MiB&lt;/td&gt;
&lt;td&gt;58.6 ms&lt;/td&gt;
&lt;td&gt;74.0 ms&lt;/td&gt;
&lt;td&gt;+15.4 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 MiB&lt;/td&gt;
&lt;td&gt;116.0 ms&lt;/td&gt;
&lt;td&gt;121.3 ms&lt;/td&gt;
&lt;td&gt;+5.3 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The environment was 2026-10-10, ap-northeast-1, FSx for ONTAP Single-AZ first generation, 128 MBps, SSD 1,024 GiB, from a local machine outside AWS over the internet (round-trip time included), concurrency 1. It is one measurement: 166 to 407 requests on the SSD side and 10 requests after the change.&lt;/p&gt;

&lt;p&gt;I cannot read this difference as a delay from reading the capacity pool, for three reasons.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the read window after the change, the per-volume &lt;code&gt;CapacityPoolReadOperations&lt;/code&gt; was 0. The reads did not reach the capacity pool.&lt;/li&gt;
&lt;li&gt;The 5 MiB difference (+5.3 ms) is smaller than the spread from p50 to p90 on the SSD side (9.0 ms).&lt;/li&gt;
&lt;li&gt;I did not alternate SSD-side and post-change reads, and the request counts are asymmetric.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Source of this table: &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md#L243-L247" rel="noopener noreferrer"&gt;Reads by tier&lt;/a&gt; (Japanese)&lt;/p&gt;

&lt;p&gt;Before you read much Tiering All data through S3 Access Points, look at the per-volume &lt;code&gt;CapacityPoolReadOperations&lt;/code&gt; one-minute values in your own environment and then estimate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choosing between S3 bucket first and file system first
&lt;/h3&gt;

&lt;p&gt;Laid out at the same granularity, the constraints of the two setups are as follows. For monthly cost, treat the rows as guidance under the base assumptions of the estimate above.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Fits when&lt;/th&gt;
&lt;th&gt;Constraints&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;S3 Files (S3 bucket first)&lt;/td&gt;
&lt;td&gt;You want the source of truth in an S3 bucket. Clients are Linux on AWS and speak NFSv4.1 or later. Objects of 128 KiB or more dominate. You do not want a fixed cost&lt;/td&gt;
&lt;td&gt;Bucket versioning required. Linux on AWS only. NFSv4.1 / 4.2 only (NFSv3, SMB, and nconnect unsupported). About 60 seconds from file to bucket. Objects below 128 KiB incur high-performance storage charges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FSx for ONTAP + S3 Access Points (file system first)&lt;/td&gt;
&lt;td&gt;You want the source of truth in an ONTAP volume. You have NFSv3 or SMB clients. You read from outside AWS by mounting over NFS or SMB (I did not measure this). Objects below 128 KiB dominate&lt;/td&gt;
&lt;td&gt;A fixed cost of $269.57 a month or more. Capacity pool read latency is unmeasured, and read request charges can apply. Clients share the throughput capacity. S3 event notifications, lifecycle, and versioning are not supported on the S3 Access Points side&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If your clients are Linux on AWS and you handle objects of 128 KiB or more while keeping the source of truth in an S3 bucket, start evaluating with S3 Files. If you have NFSv3 or SMB clients, read from outside AWS by mounting over NFS or SMB (I did not measure this), or mostly have objects below 128 KiB, start with FSx for ONTAP and S3 Access Points.&lt;/p&gt;

&lt;p&gt;The object-size conditions are guidance drawn from the 64 KiB and 4 MiB results I estimated. I did not estimate sizes near 128 KiB. Decide the monthly order only after you replace the values in "Assumptions under which the order can flip" with those of your environment and check.&lt;/p&gt;

&lt;h2&gt;
  
  
  An invalid warm / cold comparison: lowering the threshold does not evict data already loaded
&lt;/h2&gt;

&lt;p&gt;My first warm / cold comparison was invalid because both reads were warm. I have not confirmed the latency difference between warm and cold. Here warm means reading data held in high-performance storage, and cold means reading straight from the bucket.&lt;/p&gt;

&lt;p&gt;The goal was to check the claim that only frequently read data becomes low-latency. I assumed that lowering the import threshold (&lt;code&gt;sizeLessThan&lt;/code&gt;, default: import files under 128 KiB) and then reading the same file would make the read cold, and I measured on that assumption.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it was invalid: the threshold only decides what the next first access imports
&lt;/h3&gt;

&lt;p&gt;The threshold decides what gets imported the next time a directory is accessed for the first time. Data already in high-performance storage is not evicted when you lower it. Even in the window I meant to be cold, CloudWatch &lt;code&gt;DataReadBytes&lt;/code&gt; (bytes read from high-performance storage) was nonzero, so both reads came from high-performance storage.&lt;/p&gt;

&lt;p&gt;The numbers gave it away. The supposedly cold read was faster than the warm one, at 2.94 ms, which cannot be right. The record reads that difference as run-to-run variation and remount differences.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three reads: two are valid
&lt;/h3&gt;

&lt;p&gt;The environment was ap-northeast-1, 2026-10-10, c5n.9xlarge, 64 KiB, &lt;code&gt;o_direct&lt;/code&gt;, concurrency 1, p50 of n=30.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Read&lt;/th&gt;
&lt;th&gt;p50&lt;/th&gt;
&lt;th&gt;Valid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;High-performance storage (threshold 131072, 64 KiB already imported)&lt;/td&gt;
&lt;td&gt;5.33 ms&lt;/td&gt;
&lt;td&gt;Valid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same file after lowering the threshold to 1024&lt;/td&gt;
&lt;td&gt;2.94 ms&lt;/td&gt;
&lt;td&gt;Invalid (both warm)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A newly created 64 KiB file with the threshold left at 1024 (direct from the bucket)&lt;/td&gt;
&lt;td&gt;4.05 ms&lt;/td&gt;
&lt;td&gt;Valid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source of this table: &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md#L175-L179" rel="noopener noreferrer"&gt;Three reads&lt;/a&gt; (Japanese)&lt;/p&gt;

&lt;p&gt;The two valid rows, 5.33 ms and 4.05 ms, differ by an amount within run-to-run variation. A separate t3.small record showed 10.8 ms from high-performance storage and 55.0 ms straight from the bucket (2026-09-01), but that did not reproduce on c5n.9xlarge. The environments and methods differ, so it is not a comparison that varies only the path in one environment.&lt;/p&gt;

&lt;p&gt;To measure cold, read a new file that has not been imported after lowering the threshold, and confirm the path with &lt;code&gt;DataReadBytes&lt;/code&gt;. The 2.94 ms in this post cannot serve as evidence for a warm / cold difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Status of the four stated values: I got numbers for two
&lt;/h2&gt;

&lt;p&gt;Of the four values AWS states, I got numbers for two: propagation time when using both files and the S3 API, and aggregate growth with client count (up to 8). The comparison for "only frequently read data becomes low-latency" was invalid, so I cannot confirm it. I did not measure "existing tools work as they are".&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stated value&lt;/th&gt;
&lt;th&gt;What I confirmed&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Existing file-based tools work as they are&lt;/td&gt;
&lt;td&gt;The measurement tool, VDBENCH, ran on the mount. I did not look at compatibility with existing applications&lt;/td&gt;
&lt;td&gt;Not measured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The same data is usable from both files and the S3 API&lt;/td&gt;
&lt;td&gt;Propagation p50 was 63,769.3 ms (a window of about 60 seconds) from writing a file to reading it with the S3 API, and 1,533.1 ms from writing with the S3 API to reading it as a file (t3.small, NFSv4.2, 64 B, concurrency 1, n=30, 2026-09-01)&lt;/td&gt;
&lt;td&gt;Measured. File to S3 has a window of about 60 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Only frequently read data sits in high-performance storage and gets low latency&lt;/td&gt;
&lt;td&gt;64 KiB p50 was 5.33 ms from high-performance storage and 4.05 ms straight from the bucket (c5n.9xlarge, n=30, 2026-10-10), within run-to-run variation. The first comparison was invalid&lt;/td&gt;
&lt;td&gt;Not confirmed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Many clients can use it at once&lt;/td&gt;
&lt;td&gt;Aggregate 784.4 to 6,282.6 MB/s (1 to 8 clients, 8.01x, c5n.2xlarge, 2026-10-10). I did not measure the 25,000-connection and TB/s-class limits&lt;/td&gt;
&lt;td&gt;Measured up to 8 clients&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sources of this table: &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-measured.md#L42-L45" rel="noopener noreferrer"&gt;Propagation measurement&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md#L175-L179" rel="noopener noreferrer"&gt;Three reads&lt;/a&gt;, &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md#L134-L139" rel="noopener noreferrer"&gt;Client-count measurement (same)&lt;/a&gt; (Japanese)&lt;/p&gt;

&lt;p&gt;The values come with costs. Propagation from file to bucket takes about 60 seconds, the supported protocols are only NFSv4.1 and NFSv4.2, bucket versioning is required, and clients are limited to Linux on AWS. The two confirmed points are usable as grounds for adoption. Treat the low-latency value as an assumption only after you measure a cold read in your own environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I have not measured
&lt;/h2&gt;

&lt;p&gt;This post's measurements cover a single S3 Files mount (c5n.9xlarge), S3 Files client count (c5n.2xlarge, 1 to 8 clients), and latency by tier when reading through FSx for ONTAP S3 Access Points (one measurement). I did not measure the following five things.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What I have not measured (subject and object stated)&lt;/th&gt;
&lt;th&gt;Why not&lt;/th&gt;
&lt;th&gt;When not knowing hurts&lt;/th&gt;
&lt;th&gt;How to measure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;For S3 Files, I did not compare, under the same conditions, the read latency of data held in high-performance storage with the read latency of a first read straight from the bucket (the warm / cold difference).&lt;/td&gt;
&lt;td&gt;The first comparison read from high-performance storage both times, because lowering the threshold did not evict the loaded data. See the chapter "An invalid warm / cold comparison".&lt;/td&gt;
&lt;td&gt;You design on the assumption that only frequently read data is low-latency and do not budget for first-access latency.&lt;/td&gt;
&lt;td&gt;In order: (1) lower the import threshold; (2) after lowering it, create a new file that has not been imported yet; (3) read that file and check that &lt;code&gt;DataReadBytes&lt;/code&gt; (reads from high-performance storage) is not rising in the window, to confirm the path. Under the same conditions, also read a file held in high-performance storage and take the difference.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;For S3 Files, I did not measure where the aggregate throughput stops growing when 9 or more clients read at once. I measured 1, 2, 4, and 8 c5n.2xlarge clients. The published limits are 25,000 connections per file system and an aggregate read of "up to terabytes per second" (&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-quotas.html" rel="noopener noreferrer"&gt;quotas&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files-performance.html" rel="noopener noreferrer"&gt;Performance specifications&lt;/a&gt;).&lt;/td&gt;
&lt;td&gt;My measurement plan stopped at 8 clients, and instance costs grow with the client count.&lt;/td&gt;
&lt;td&gt;You design for dozens of clients sharing it and multiply roughly 785 MB/s per client by the client count as is.&lt;/td&gt;
&lt;td&gt;With the same procedure (&lt;code&gt;s4-ladder.sh&lt;/code&gt;), raise the client count to 16 and 32 and look for the count where per-client throughput starts to fall.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;For S3 Files metadata charges, I could not confirm where the number of operations per read comes from, whether it reaches the bill, or how metadata writes on import are metered. All I know is one observation (m5.large, 2026-10-16): for a 4 MiB read, &lt;code&gt;MetadataReadBytes&lt;/code&gt; registered about 6.0 times per object.&lt;/td&gt;
&lt;td&gt;I did not capture the breakdown of the roughly 6 (open, getattr, or others), and I have not confirmed whether the reads went through NFS or directly to the bucket. Cost Explorer right after the measurement showed no usage, so I cannot tell whether it is the same as the billing meter. &lt;code&gt;MetadataWriteBytes&lt;/code&gt; on import had 0 data points, and I could not tell whether no ingest happened or whether it is not metered.&lt;/td&gt;
&lt;td&gt;You use the "Of which S3 Files metadata charges" column of the cost tables as a bill estimate.&lt;/td&gt;
&lt;td&gt;After a few days, reconcile the Cost Explorer usage (&lt;code&gt;APN1-Files-Read&lt;/code&gt;) against the sum of &lt;code&gt;DataReadBytes&lt;/code&gt; and &lt;code&gt;MetadataReadBytes&lt;/code&gt;. Take the per-operation breakdown from NFS operation statistics. For ingest, also compare the increase in &lt;code&gt;Inodes&lt;/code&gt; before and after.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;For FSx for ONTAP S3 Access Points, I did not confirm how many read requests one &lt;code&gt;GetObject&lt;/code&gt; of an object on the capacity pool generates on the capacity pool.&lt;/td&gt;
&lt;td&gt;In the measured window (33 reads), the per-volume &lt;code&gt;CapacityPoolReadOperations&lt;/code&gt; was 0. The reads did not reach the capacity pool, so there is no numerator to derive a count from. This is not something waiting for Cost Explorer to fill in.&lt;/td&gt;
&lt;td&gt;You build an estimate that puts read-heavy data on a Tiering All volume.&lt;/td&gt;
&lt;td&gt;First confirm the conditions under which reads reach the capacity pool. In a window where they do, read the per-volume &lt;code&gt;CapacityPoolReadOperations&lt;/code&gt; one-minute values and divide by the number of reads. Use &lt;code&gt;CPoolStdRd&lt;/code&gt; in Cost Explorer for the later reconciliation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;I did not measure the latency difference of reading capacity pool data in a Multi-AZ configuration, from an on-premises client, or from inside a VPC (no internet round trip). I measured one time, from a local machine outside AWS over the internet.&lt;/td&gt;
&lt;td&gt;My test environment was a single Single-AZ file system, and the client was one machine outside AWS.&lt;/td&gt;
&lt;td&gt;Your configuration (client location, AZ layout) differs from my measurement, and you apply +5.3 to +15.4 ms as is.&lt;/td&gt;
&lt;td&gt;Put the client on EC2 in the same VPC and alternate reads of the same size from an SSD volume and a capacity pool volume.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If any row has conditions that differ from yours, measure that row first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing it yourself: three runbooks from creation to teardown
&lt;/h2&gt;

&lt;p&gt;You can follow this post's measurements in your own AWS account with the same steps. I put runbooks from creating the environment to tearing it down on GitHub. The links are pinned to the commit of the measurement.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create the environment and mount: &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/deployment/aws-s3files-compare.md" rel="noopener noreferrer"&gt;S3 Files verification environment setup&lt;/a&gt; (Japanese). It builds with CloudFormation and &lt;code&gt;runbook.sh&lt;/code&gt;. The most frequent cause of a silently hanging mount (missing CloudWatch permissions on the host role) is written at the start of these steps.&lt;/li&gt;
&lt;li&gt;Measure throughput and client count: from the section "Measuring throughput and client count" on the same page, go to &lt;code&gt;throughput-runbook.sh&lt;/code&gt; (single mount) and &lt;code&gt;s4-ladder.sh&lt;/code&gt; (1 to 8 clients). VDBENCH requires accepting Oracle's license, so obtain it separately.&lt;/li&gt;
&lt;li&gt;Results and conditions: &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files/blob/90c24f4bf4f89ad41386869586a8cfd9283a353c/docs/ja/verification/s3files-throughput-measured.md" rel="noopener noreferrer"&gt;S3 Files file system performance&lt;/a&gt; (Japanese). This is where this post's numbers come from, each with its environment. It also states that each is a single sample.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Leaving the verification environment up keeps billing. When you finish measuring, tear it down with the runbook's &lt;code&gt;destroy&lt;/code&gt; and &lt;code&gt;stop&lt;/code&gt;, then confirm by count that nothing remains.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Two things hold from the measurements, and one from the estimate.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The single-mount ceiling was one efs-proxy CPU core. It was 1,104.9 MB/s for 1 MiB sequential reads on a c5n.9xlarge (one run, 2026-10-10, ap-northeast-1).&lt;/li&gt;
&lt;li&gt;When bandwidth falls short, adding clients raised it. From 1 to 8 c5n.2xlarge clients, the aggregate went from 784.4 MB/s to 6,282.6 MB/s (8.01x). I have not measured 9 or more.&lt;/li&gt;
&lt;li&gt;In the estimate, under this model's base assumptions, the monthly cost order split by object size, not by read and write volume. The candidate for the dividing line is around the S3 Files import threshold (128 KiB), and I estimated only two sizes, 64 KiB and 4 MiB. With the stored volume fixed at 20 TiB and five patterns (P1, P4, P5, P6, P8), 64 KiB comes to $1,783.11 to $2,218.13 a month on FSx for ONTAP and $7,928.32 to $12,030.81 via S3 Files, and 4 MiB comes to $512.14 to $543.52 via S3 Files and $1,781.28 to $1,788.08 on FSx for ONTAP (ap-northeast-1, subtotals of the main line items with prices read on 2026-08-09; list price x quantity, not an invoice). This order is a result under the model's base assumptions and can flip when they change, so check it against your own values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which one to consider first narrows down by where the source of truth lives and where clients read from. Because the monthly order by object size can change with assumptions, treat the object-size conditions in the next table as guidance under the base assumptions of the estimate.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Check first&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The source of truth is an S3 bucket. Clients are Linux on AWS and can use NFSv4.1 or later. Objects of 128 KiB or more dominate&lt;/td&gt;
&lt;td&gt;S3 Files&lt;/td&gt;
&lt;td&gt;Bucket versioning, about 60 seconds from file to bucket, and that NFSv3 and SMB are not supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The source of truth is an ONTAP volume. You have NFSv3 or SMB clients, read from outside AWS by mounting over NFS or SMB (I did not measure this), or mostly have small objects below 128 KiB&lt;/td&gt;
&lt;td&gt;FSx for ONTAP with S3 Access Points&lt;/td&gt;
&lt;td&gt;A fixed cost of $269.57 a month or more, capacity pool read request charges, and that clients share the throughput capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warm / cold latency, 9 or more clients, or the capacity pool read count will decide your design&lt;/td&gt;
&lt;td&gt;Measure in your own environment, then decide&lt;/td&gt;
&lt;td&gt;The rows of the "What I have not measured" table whose conditions differ from yours&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I keep every measured value, each with its environment, on &lt;a href="https://github.com/Yoshiki0705/s3-burst-on-ontap-files" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. I hope this helps anyone with a similar need (collect in S3, read as files). &amp;lt;!-- allow:ai-style the "I hope this helps" closing mirrors the JA 一助になれば幸いです and is the author's fixed first-person sign-off (global-author-voice); HTML comments are stripped on publish so the visible text is unchanged --&amp;gt;&lt;/p&gt;

&lt;p&gt;Until next time.&lt;/p&gt;

&lt;p&gt;Yoshiki Fujiwara&lt;/p&gt;

</description>
      <category>s3files</category>
      <category>aws</category>
      <category>s3</category>
      <category>storage</category>
    </item>
    <item>
      <title>How AWS Bedrock AgentCore Actually Works</title>
      <dc:creator>Pin Xiong</dc:creator>
      <pubDate>Fri, 16 Oct 2026 09:20:35 +0000</pubDate>
      <link>https://dev.to/aws-builders/how-aws-bedrock-agentcore-actually-works-2ckb</link>
      <guid>https://dev.to/aws-builders/how-aws-bedrock-agentcore-actually-works-2ckb</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on &lt;a href="https://xiongpin.dev/posts/how-aws-bedrock-agentcore-actually-works/?utm_source=dev.to"&gt;xiongpin.dev&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AWS Bedrock AgentCore replaces Dockerfiles, instance sizing and hand-rolled IAM with a single JSON file. One command turns that file into a running, observable endpoint. This walkthrough covers the full path from project creation to trace analysis.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://xiongpin.dev/images/how-aws-bedrock-agentcore-actually-works/F0-command-map.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7kqr5hbzouvcq39duu2o.png" alt="The whole AgentCore CLI lifecycle in six commands" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What AgentCore is and is not
&lt;/h2&gt;

&lt;p&gt;AgentCore is a family of managed runtimes for AI agents: nine managed services covering hosting, memory, access control, tool integration, code execution, browser automation, quality and observability. This article covers two of them: Runtime and Observability.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://xiongpin.dev/images/how-aws-bedrock-agentcore-actually-works/F1-capability-map.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx8nxjl6swouqpamqbba8.png" alt="AgentCore: 9 managed services, 2 used in this article" width="800" height="643"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What it is not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Not a new agent framework. You still write your agent in Strands, LangChain, the OpenAI SDK or raw Python.&lt;/li&gt;
&lt;li&gt;Not container orchestration. Each session gets its own microVM and the platform handles scaling.&lt;/li&gt;
&lt;li&gt;Not a model router. Your code calls Bedrock the way it would anywhere else.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AgentCore hosts that code and adds an HTTP endpoint, an IAM role and a trace pipeline. Readers who have run ECS or Fargate will notice the difference at once: no Dockerfile, no task definition, no target group.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three layers, one direction
&lt;/h2&gt;

&lt;p&gt;You write agent code. You write two JSON files. The CLI generates everything else. AWS runs a CloudFormation stack that you never have to look at.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://xiongpin.dev/images/how-aws-bedrock-agentcore-actually-works/F2-three-layers.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F995r57zn8uasukfmv4z1.png" alt="Three layers: business code, the contract, the plumbing" width="800" height="584"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What lives there&lt;/th&gt;
&lt;th&gt;Who maintains it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Business code&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;app/&amp;lt;AgentName&amp;gt;/&lt;/code&gt; with the agent entrypoint&lt;/td&gt;
&lt;td&gt;you&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The contract&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;agentcore.json&lt;/code&gt; and &lt;code&gt;aws-targets.json&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;you, by hand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The plumbing&lt;/td&gt;
&lt;td&gt;a CDK project under &lt;code&gt;agentcore/cdk/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;the CLI, on every deploy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The design follows one principle: declared resources live in JSON; everything the CLI generates is regenerable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The environment you need
&lt;/h2&gt;

&lt;p&gt;Requirements:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Version used&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;24.x&lt;/td&gt;
&lt;td&gt;20+ required by the CLI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;node --version&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AgentCore CLI&lt;/td&gt;
&lt;td&gt;0.31.1&lt;/td&gt;
&lt;td&gt;install below&lt;/td&gt;
&lt;td&gt;&lt;code&gt;agentcore --version&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS CLI&lt;/td&gt;
&lt;td&gt;2.x&lt;/td&gt;
&lt;td&gt;any recent build&lt;/td&gt;
&lt;td&gt;&lt;code&gt;aws --version&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uv&lt;/td&gt;
&lt;td&gt;0.11.9&lt;/td&gt;
&lt;td&gt;the CLI builds the Python environment for template agents with it&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uv --version&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Git&lt;/td&gt;
&lt;td&gt;2.39&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;create&lt;/code&gt; runs &lt;code&gt;git init&lt;/code&gt; in the new project&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git --version&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;3.12 in the app venv&lt;/td&gt;
&lt;td&gt;3.10 to 3.14 supported by CodeZip; the runtime target is the CLI's default, not your interpreter&lt;/td&gt;
&lt;td&gt;&lt;code&gt;python3 -V&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS account&lt;/td&gt;
&lt;td&gt;any&lt;/td&gt;
&lt;td&gt;with permissions below&lt;/td&gt;
&lt;td&gt;&lt;code&gt;aws sts get-caller-identity&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region&lt;/td&gt;
&lt;td&gt;us-east-1&lt;/td&gt;
&lt;td&gt;the whole series uses it&lt;/td&gt;
&lt;td&gt;&lt;code&gt;aws configure get region&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;td&gt;not needed&lt;/td&gt;
&lt;td&gt;this series uses CodeZip builds&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Install the CLI and set the region:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @aws/agentcore@latest
aws configure &lt;span class="nb"&gt;set &lt;/span&gt;region us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Self-check commands and expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="c"&gt;# v24.14.0&lt;/span&gt;

uv &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="c"&gt;# uv 0.11.9&lt;/span&gt;

git &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="c"&gt;# git version 2.39.5&lt;/span&gt;

agentcore &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="c"&gt;# 0.31.1&lt;/span&gt;

aws sts get-caller-identity &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'[Account,Arn]'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text
&lt;span class="c"&gt;# 123456789012    arn:aws:iam::123456789012:user/yourname&lt;/span&gt;

aws configure get region
&lt;span class="c"&gt;# us-east-1&lt;/span&gt;

aws bedrock list-foundation-models &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'modelSummaries[?modelId==`deepseek.v3.2`].modelId'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text
&lt;span class="c"&gt;# deepseek.v3.2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://xiongpin.dev/images/how-aws-bedrock-agentcore-actually-works/screenshot-1-environment-check.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fty31rikn5usdcpo7z0j6.png" alt="Terminal self-check: tool versions, AWS identity, region and model availability" width="800" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Runtime IAM role. The CLI generates an IAM role for the runtime. The policy carries three permission groups: &lt;code&gt;bedrock:InvokeModel*&lt;/code&gt;, &lt;code&gt;logs:*&lt;/code&gt; for the group under &lt;code&gt;/aws/bedrock-agentcore/runtimes/&lt;/code&gt;, and &lt;code&gt;xray:PutTraceSegments&lt;/code&gt;. It is a least-privilege template for later.&lt;/p&gt;

&lt;p&gt;Known issue: if &lt;code&gt;agentcore --version&lt;/code&gt; errors but the command exists, an old Python package (&lt;code&gt;bedrock-agentcore-starter-toolkit&lt;/code&gt;) is shadowing the npm one. &lt;code&gt;pip uninstall bedrock-agentcore-starter-toolkit&lt;/code&gt; fixes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 · Scaffold the project
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore create &lt;span class="nt"&gt;--name&lt;/span&gt; hello_agent &lt;span class="nt"&gt;--project-name&lt;/span&gt; helloagent &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--framework&lt;/span&gt; Strands &lt;span class="nt"&gt;--model-provider&lt;/span&gt; Bedrock &lt;span class="nt"&gt;--memory&lt;/span&gt; none &lt;span class="nt"&gt;--defaults&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[done]  Create helloagent/ project directory
[done]  Prepare agentcore/ directory
[done]  Initialize git repository
[done]  Add agent to project
[done]  Set up Python environment

Created:
  helloagent/
    app/hello_agent/  Python agent (Strands)
    agentcore/        Config and CDK project

Project created successfully!

To continue, navigate to your new project:

  cd helloagent
  agentcore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command produces three categories of files:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://xiongpin.dev/images/how-aws-bedrock-agentcore-actually-works/screenshot-2-directory-tree.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwe9wrrhzor5eohjt7gma.png" alt="Project tree in the editor: agentcore/ config and app/hello_agent/ code" width="800" height="965"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project root gets &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;README.md&lt;/code&gt; and &lt;code&gt;git init&lt;/code&gt;. &lt;code&gt;agentcore/&lt;/code&gt; gets the contract (&lt;code&gt;agentcore.json&lt;/code&gt;), the targets file (&lt;code&gt;aws-targets.json&lt;/code&gt;), &lt;code&gt;.env.local&lt;/code&gt; for secrets and the vendored &lt;code&gt;cdk/&lt;/code&gt; app. &lt;code&gt;app/hello_agent/&lt;/code&gt; gets the agent: &lt;code&gt;main.py&lt;/code&gt; (entrypoint), a &lt;code&gt;model/&lt;/code&gt; package, &lt;code&gt;pyproject.toml&lt;/code&gt; and helper packages (&lt;code&gt;mcp_client/&lt;/code&gt;, &lt;code&gt;skills/&lt;/code&gt;). The contract points &lt;code&gt;codeLocation&lt;/code&gt; at &lt;code&gt;app/hello_agent/&lt;/code&gt; and &lt;code&gt;entrypoint&lt;/code&gt; at &lt;code&gt;main.py&lt;/code&gt;; the platform zips, deploys and starts what &lt;code&gt;codeLocation&lt;/code&gt; names.&lt;/p&gt;

&lt;p&gt;Inside &lt;code&gt;agentcore/&lt;/code&gt;, only &lt;code&gt;agentcore.json&lt;/code&gt; and &lt;code&gt;aws-targets.json&lt;/code&gt; are yours to edit; the rest is regenerated on every deploy. The CLI detects a project by walking up from the current directory until it finds a folder holding both &lt;code&gt;agentcore/&lt;/code&gt; and &lt;code&gt;agentcore/agentcore.json&lt;/code&gt; (missing either reports &lt;code&gt;No agentcore project found&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Every resource array except &lt;code&gt;runtimes&lt;/code&gt; starts empty, and &lt;code&gt;aws-targets.json&lt;/code&gt; is &lt;code&gt;[]&lt;/code&gt;. You fill both next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 · Write the contract: two JSON files
&lt;/h2&gt;

&lt;p&gt;The deployment definition, trimmed to the fields that matter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.agentcore.aws.dev/v1/agentcore.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"helloagent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"managedBy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CDK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"runtimes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hello_agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CodeZip"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"entrypoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"codeLocation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app/hello_agent/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"runtimeVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PYTHON_3_14"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"networkMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PUBLIC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"protocol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HTTP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"envVars"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MODEL_ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deepseek.v3.2"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"memories"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"knowledgeBases"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"agentCoreGateways"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;create&lt;/code&gt; writes this file with every resource array empty; &lt;code&gt;agentcore add&lt;/code&gt; (&lt;code&gt;add agent&lt;/code&gt;, &lt;code&gt;add memory&lt;/code&gt;, &lt;code&gt;add gateway&lt;/code&gt;) pushes entries in.&lt;/p&gt;

&lt;p&gt;Four fields in the runtime block cannot be wrong without a failed deploy: &lt;code&gt;entrypoint&lt;/code&gt; (the file the platform calls), &lt;code&gt;codeLocation&lt;/code&gt; (&lt;code&gt;app/hello_agent/&lt;/code&gt;), &lt;code&gt;build&lt;/code&gt; (&lt;code&gt;CodeZip&lt;/code&gt;, meaning no container) and &lt;code&gt;protocol&lt;/code&gt; (&lt;code&gt;HTTP&lt;/code&gt;). The rest have defaults. &lt;code&gt;envVars&lt;/code&gt; is how &lt;code&gt;MODEL_ID&lt;/code&gt; reaches your code, so switching models is a JSON edit rather than a code change.&lt;/p&gt;

&lt;p&gt;Check that &lt;code&gt;model/load.py&lt;/code&gt; reads the model from the &lt;code&gt;MODEL_ID&lt;/code&gt; environment variable rather than a hardcoded ID (the generated template sometimes defaults to a Claude model):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.models.bedrock&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BedrockModel&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_model&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;BedrockModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;model_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MODEL_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deepseek.v3.2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;BedrockModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validate the contract before deploying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore validate
&lt;span class="c"&gt;# Valid&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second file decides where the deployment lands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"account"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123456789012"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"region"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;create&lt;/code&gt; writes only an empty array. The first &lt;code&gt;deploy&lt;/code&gt; fills it in: it calls STS, learns who you are and writes the target back into the file. There is no &lt;code&gt;--region&lt;/code&gt; flag on deploy, status or remove, so the file is the only source of truth for where your agent lands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 · Deploy: one command, six steps
&lt;/h2&gt;

&lt;p&gt;From the project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore deploy &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--target&lt;/span&gt; default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://xiongpin.dev/images/how-aws-bedrock-agentcore-actually-works/screenshot-3-first-deploy.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwmajx3dy2jsvk230deog.png" alt="agentcore deploy progress and the stack outputs, including the runtime ID and ARN" width="799" height="521"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Between the command and the last checkmark, six things happen:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://xiongpin.dev/images/how-aws-bedrock-agentcore-actually-works/F4-deploy-pipeline.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fie4k9cx18nyxtui3yp5x.png" alt="Deploy: six steps to READY, with measured timings" width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Package the code. The CLI zips whatever &lt;code&gt;codeLocation&lt;/code&gt; names, with platform dependencies, into one asset. A hello-world agent produces a 41 MB zip; the local &lt;code&gt;.venv&lt;/code&gt; is excluded. No image is built anywhere in the project.&lt;/li&gt;
&lt;li&gt;Synthesize. The vendored CDK app (&lt;code&gt;node dist/bin/cdk.js&lt;/code&gt;) reads both JSON files and emits a CloudFormation template into &lt;code&gt;cdk.out/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Upload the asset. The zip lands in the S3 bucket that &lt;code&gt;cdk bootstrap&lt;/code&gt; created once per account and region. Without that one-time bootstrap, no CDK deployment can store an asset.&lt;/li&gt;
&lt;li&gt;Create the stack. CloudFormation provisions four resources: the runtime, an IAM role for it, a Bedrock model-access policy and a CDK metadata resource.&lt;/li&gt;
&lt;li&gt;Record the state. The CLI writes &lt;code&gt;agentcore/.cli/deployed-state.json&lt;/code&gt; with the runtime ARN and endpoint.&lt;/li&gt;
&lt;li&gt;Print the endpoint.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The synthesis step is where schema-first becomes visible. The generated app reads your files by path and carries no project knowledge of its own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// agentcore/cdk/bin/cdk.ts, verbatim from the generated project&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;configRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;..&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;configIO&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConfigIO&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;baseDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;configRoot&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;spec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;configIO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readProjectSpec&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;             &lt;span class="c1"&gt;// agentcore.json&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;configIO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readAWSDeploymentTargets&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// aws-targets.json&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generated CDK app reads &lt;code&gt;agentcore.json&lt;/code&gt; and &lt;code&gt;aws-targets.json&lt;/code&gt; by path and carries no project-specific logic; it is a compiler driven entirely by the two files.&lt;/p&gt;

&lt;p&gt;Measured timings: this deploy took 3 minutes 29 seconds. The CLI's step log says where it went: building the CDK project 25 s, synthesizing CloudFormation 1 m 3 s, creating the stack 1 m 51 s. The first deploy ever in this account took 9 minutes 49 seconds; the one-time &lt;code&gt;cdk bootstrap&lt;/code&gt; and the CDK project's first &lt;code&gt;npm install&lt;/code&gt; account for the difference. A code-only redeploy took 1 minute 26 seconds, because the stack already exists and only the asset hash changes. Verify with &lt;code&gt;agentcore status&lt;/code&gt;, which reports &lt;code&gt;Runtime: READY&lt;/code&gt; plus the endpoint URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 · Run the same code locally
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;agentcore dev -l&lt;/code&gt; runs the same app locally on &lt;code&gt;localhost:8080&lt;/code&gt;. Model calls still go to Bedrock, so credentials are needed but no deployment is. Edits to &lt;code&gt;main.py&lt;/code&gt; trigger hot reload without a restart.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://xiongpin.dev/images/how-aws-bedrock-agentcore-actually-works/screenshot-4-local-run.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyvgwelwibid4tpdjha6q.png" alt="agentcore dev -l boot banner: Uvicorn on localhost:8080 with the reloader watching app/hello_agent/" width="800" height="285"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 · Invoke the deployed runtime
&lt;/h2&gt;

&lt;p&gt;The deployed runtime answers through the data plane. The CLI wraps it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore invoke &lt;span class="nt"&gt;--prompt&lt;/span&gt; &lt;span class="s2"&gt;"What is 17 plus 25? Use the tool."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://xiongpin.dev/images/how-aws-bedrock-agentcore-actually-works/screenshot-5-cloud-invoke.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8itehcf5hovw7aj7gcwn.png" alt="agentcore invoke against the deployed runtime: the reply, the session ID and the invoke log path" width="800" height="150"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The answer is correct and the tool ran, but the reply carries model scaffolding around it: the &lt;code&gt;I'll use the addition tool&lt;/code&gt; preamble and a &lt;code&gt;&amp;lt;|DSML|&amp;gt;function_calls&lt;/code&gt; marker precede the final text. That is model behavior, not a platform bug; the troubleshooting section explains the fix.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://xiongpin.dev/images/how-aws-bedrock-agentcore-actually-works/F5-one-surface.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg9okmpclwrbg6ce0f11a.png" alt="One HTTP surface, two hosts: agentcore dev and agentcore deploy" width="800" height="584"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;agentcore dev&lt;/code&gt; and the deployed runtime are two hosts for the same app: the local server on &lt;code&gt;localhost:8080&lt;/code&gt;, and a managed microVM behind &lt;code&gt;https://bedrock-agentcore.&amp;lt;region&amp;gt;.amazonaws.com/runtimes/&amp;lt;runtime-arn&amp;gt;/invocations&lt;/code&gt;. Both expose the same two routes (&lt;code&gt;/ping&lt;/code&gt;, &lt;code&gt;/invocations&lt;/code&gt;); routing, scaling and TLS stay on the platform side, so the code is unaware which host it runs on. &lt;code&gt;agentcore invoke&lt;/code&gt; is a CLI client for the hosted &lt;code&gt;/invocations&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Cost per invoke: DeepSeek V3.2 in us-east-1 charges $0.62 per million input tokens and $1.85 per million output. A two-cycle invoke used 846 input and 82 output tokens: about $0.0007 of model calls plus $0.0002 of runtime compute. For a tutorial-grade agent, the infrastructure bill rounds to zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood I: the HTTP contract
&lt;/h2&gt;

&lt;p&gt;The runtime makes two requests to your code: &lt;code&gt;GET /ping&lt;/code&gt; for health checks and &lt;code&gt;POST /invocations&lt;/code&gt; for work. &lt;code&gt;BedrockAgentCoreApp&lt;/code&gt; is a thin adapter mapping those routes onto an async generator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bedrock_agentcore.runtime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BedrockAgentCoreApp&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BedrockAgentCoreApp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_numbers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return the sum of two numbers.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;

&lt;span class="nd"&gt;@app.entrypoint&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;session_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default-session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_or_create_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;extract_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three design details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The entrypoint is an async generator. Every yielded dict becomes one server-sent event, so streaming is the platform's default shape, not an add-on.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;context.session_id&lt;/code&gt; is the only state key you manage. Each session runs in its own microVM; the platform handles scaling.&lt;/li&gt;
&lt;li&gt;A production entrypoint also filters text deltas through a stream sanitizer; this demo runs without one, so the leak is visible in the invoke screenshot (see Troubleshooting).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Under the hood II: what observability records
&lt;/h2&gt;

&lt;p&gt;A deployed invoke produces this span tree in X-Ray. The output is real, including the token counts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore traces list          &lt;span class="c"&gt;# lists recent traces (2-3 min after the invoke)&lt;/span&gt;
agentcore traces get &amp;lt;trace-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://xiongpin.dev/images/how-aws-bedrock-agentcore-actually-works/screenshot-6-observability.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhka4dowsrj2m5ht6bmmx.png" alt="agentcore traces list and traces get: the trace for the invoke above tops the list" width="800" height="402"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;invoke_agent Strands Agents              gen_ai.agent.tools=["add_numbers"]  tokens=902
├── execute_event_loop_cycle  (cycle 1)
│   ├── execute_tool add_numbers         gen_ai.tool.status=success  span_kind=TOOL
│   └── chat  deepseek.v3.2 (LLM, 435 tokens)
│       └── chat deepseek.v3.2 (Bedrock)   finish_reason=tool_use
└── execute_event_loop_cycle  (cycle 2)
    └── chat  deepseek.v3.2 (LLM, 467 tokens)
        └── chat deepseek.v3.2 (Bedrock)   finish_reason=end_turn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The structure teaches the agent loop without reading framework source: one cycle per model round trip, the tool call as its own span inside the cycle, and a second cycle that consumes the result.&lt;/p&gt;

&lt;p&gt;Two caveats from measurement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spans are asynchronous. Transaction search takes about ten minutes before anything is queryable, so a test that invokes and greps CloudWatch immediately will fail.&lt;/li&gt;
&lt;li&gt;Local &lt;code&gt;agentcore dev&lt;/code&gt; traces record tool calls in logs but not as spans. Only the deployed runtime emits the &lt;code&gt;execute_tool&lt;/code&gt; span. A smoke test that asserts on a tool span must run against the deployment.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cleanup
&lt;/h2&gt;

&lt;p&gt;Two commands bring the whole thing down, run from the project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore remove all &lt;span class="nt"&gt;-y&lt;/span&gt;     &lt;span class="c"&gt;# empties every resource array in agentcore.json&lt;/span&gt;
agentcore deploy &lt;span class="nt"&gt;-y&lt;/span&gt;         &lt;span class="c"&gt;# converges the stack to an empty one&lt;/span&gt;
agentcore status            &lt;span class="c"&gt;# nothing deployed should be listed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;remove all&lt;/code&gt; edits the JSON; it does not touch AWS on its own. The &lt;code&gt;deploy&lt;/code&gt; that follows deletes the runtime, its IAM role and the stack. Two things survive on purpose: the CDK bootstrap stack (&lt;code&gt;CDKToolkit&lt;/code&gt;) and its asset bucket, reused by every future deployment in that account, and the CloudWatch log group under &lt;code&gt;/aws/bedrock-agentcore/runtimes/&lt;/code&gt; holding the traces. Delete those by hand if finished with the account. AgentCore bills per use, not per hour, so a deployed project left idle costs nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Stale &lt;code&gt;cdk.out&lt;/code&gt; fails the next deploy. The CLI deletes &lt;code&gt;cdk.out&lt;/code&gt; before each synth, so leftover state from a previous run can kill it with &lt;code&gt;Subprocess exited with error 1&lt;/code&gt;. Running &lt;code&gt;cdk synth&lt;/code&gt; by hand still works, confirming the project itself is fine. &lt;code&gt;rm -rf agentcore/cdk/cdk.out&lt;/code&gt; fixes it.&lt;/li&gt;
&lt;li&gt;Renaming is destroying. The &lt;code&gt;name&lt;/code&gt; field becomes the CloudFormation logical ID. Changing it does not rename the runtime; it deletes the old one and creates a new one.&lt;/li&gt;
&lt;li&gt;Models leak scaffolding into the text channel. &lt;code&gt;deepseek.v3.2&lt;/code&gt; emits fragments of internal chat-template markers as ordinary text (visible in the invoke screenshot); Nova models emit &lt;code&gt;&amp;lt;thinking&amp;gt;&lt;/code&gt; blocks. Calling Bedrock's &lt;code&gt;converse_stream&lt;/code&gt; with Strands removed proves this is model behavior, not a framework bug. The fix is a stream-safe filter in the entrypoint that holds back marker prefixes and resets once per assistant message; the official &lt;code&gt;01-features/06-observe&lt;/code&gt; sample ships a reference implementation.&lt;/li&gt;
&lt;li&gt;The region is not a flag. Every &lt;code&gt;--region&lt;/code&gt; guess fails with &lt;code&gt;unknown option&lt;/code&gt;. The targets file decides.&lt;/li&gt;
&lt;li&gt;Model fallback to Claude triggers geographic restriction. If &lt;code&gt;MODEL_ID&lt;/code&gt; is not properly loaded from the envVars, the Strands SDK defaults to a Claude model. Readers in regions where Anthropic models are unavailable see &lt;code&gt;Access to Anthropic models is not allowed from unsupported countries&lt;/code&gt;. Verify &lt;code&gt;MODEL_ID&lt;/code&gt; is in &lt;code&gt;agentcore.json&lt;/code&gt; envVars and that &lt;code&gt;model/load.py&lt;/code&gt; passes it to the &lt;code&gt;BedrockModel&lt;/code&gt; constructor explicitly, not relying on the SDK default.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The JSON file is the interface. Everything downstream (the CDK project, the template, the stack) is regenerable plumbing driven by it.&lt;/li&gt;
&lt;li&gt;CDK appears as a vendored compiler. No constructs to write, no generated project to edit. Change the schema and the same code compiles a different stack.&lt;/li&gt;
&lt;li&gt;Local and deployed share one HTTP contract. &lt;code&gt;/ping&lt;/code&gt; and &lt;code&gt;/invocations&lt;/code&gt; behave identically, so code that passed locally deploys without a shim.&lt;/li&gt;
&lt;li&gt;Observability is real but asynchronous. The tool call gets its own span in the cloud and spans take minutes to become queryable. Write tests accordingly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In AgentCore, your infrastructure is not code you maintain. It is code that compiles.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>agents</category>
      <category>agentcore</category>
    </item>
    <item>
      <title>Streaming Salesforce Events to AWS with Event Relay and EventBridge</title>
      <dc:creator>Sabarish Sathasivan</dc:creator>
      <pubDate>Thu, 15 Oct 2026 18:46:34 +0000</pubDate>
      <link>https://dev.to/aws-builders/streaming-salesforce-events-to-aws-with-event-relay-and-eventbridge-4ac2</link>
      <guid>https://dev.to/aws-builders/streaming-salesforce-events-to-aws-with-event-relay-and-eventbridge-4ac2</guid>
      <description>&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;Many organizations keep data in Salesforce and run their applications on AWS. Keeping the two in sync often goes through an integration platform such as Boomi or MuleSoft, which adds licensing cost and another system to run.&lt;/p&gt;

&lt;p&gt;This article shows how to use &lt;a href="https://help.salesforce.com/s/articleView?id=platform.ev_relay_intro.htm&amp;amp;type=5" rel="noopener noreferrer"&gt;Salesforce Event Relay&lt;/a&gt; and the &lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-saas-salesforce.html" rel="noopener noreferrer"&gt;Amazon EventBridge partner integration for Salesforce&lt;/a&gt; to synchronize data from Salesforce to AWS through EventBridge, without middleware.&lt;/p&gt;

&lt;p&gt;In the example, when a customer is created in Salesforce, the customer details are sent to EventBridge and stored in DynamoDB for the AWS applications to use. The full code, including the Salesforce metadata and the CDK stack for the AWS side, is in the &lt;a href="https://github.com/ssathasivan/salesforce-event-relay-eventbridge" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhfpxgi2p7oana2diy4se.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhfpxgi2p7oana2diy4se.png" alt="Architecture" width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Salesforce
&lt;/h3&gt;

&lt;p&gt;When a record is created, updated, or deleted, Salesforce can publish a &lt;a href="https://developer.salesforce.com/docs/platform/platform-events/guide/platform-events-intro.html" rel="noopener noreferrer"&gt;platform event&lt;/a&gt;. A platform event is a message that systems outside Salesforce can subscribe to. Salesforce doesn't publish it on its own: a record-triggered Flow or an Apex trigger publishes the event when the record changes.&lt;/p&gt;

&lt;p&gt;Published events go to the Salesforce &lt;strong&gt;event bus&lt;/strong&gt;. Subscribers read events from the bus. The bus &lt;a href="https://developer.salesforce.com/docs/platform/pub-sub-api/guide/event-message-durability.html" rel="noopener noreferrer"&gt;keeps events for 72 hours&lt;/a&gt;, which makes replay possible in case of an outage.&lt;/p&gt;

&lt;p&gt;In this example, when a customer is created, Salesforce publishes a platform event called &lt;code&gt;Customer_Created__e&lt;/code&gt; with these fields:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Account_Id__c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Text(18)&lt;/td&gt;
&lt;td&gt;Salesforce ID of the Account (customer) record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Name__c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Text(255)&lt;/td&gt;
&lt;td&gt;Customer name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Industry__c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Text(40)&lt;/td&gt;
&lt;td&gt;Customer industry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CreatedDate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Date/Time&lt;/td&gt;
&lt;td&gt;Standard field, set by Salesforce when the event is published&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CreatedById&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lookup(User)&lt;/td&gt;
&lt;td&gt;Standard field, the user who published the event&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Salesforce Event Relay
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://help.salesforce.com/s/articleView?id=platform.ev_relay_intro.htm&amp;amp;type=5" rel="noopener noreferrer"&gt;Event Relay&lt;/a&gt; is a Salesforce feature that reads events from the Salesforce event bus and delivers them to Amazon EventBridge in your AWS account. Salesforce keeps track of the last event it delivered. After a transient error, such as a connectivity issue, the relay resumes from where it left off.&lt;/p&gt;

&lt;p&gt;A relay is built from three Salesforce components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Named credential.&lt;/strong&gt; It tells Salesforce where to deliver: your AWS account ID and region, written as a URL such as &lt;code&gt;arn:aws:US-EAST-1:123456789012&lt;/code&gt;. It contains no AWS keys and no IAM role. Salesforce is a registered EventBridge partner, so &lt;a href="https://help.salesforce.com/s/articleView?id=platform.ev_relay_security.htm&amp;amp;language=en_US&amp;amp;type=5" rel="noopener noreferrer"&gt;no AWS authentication is involved&lt;/a&gt;. The region must be uppercase. Once a relay uses the credential, you &lt;a href="https://help.salesforce.com/s/articleView?id=platform.ev_relay_create_named_credential.htm&amp;amp;language=en_US&amp;amp;type=5" rel="noopener noreferrer"&gt;can't change the account or region&lt;/a&gt; without deleting and recreating the relay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Channel.&lt;/strong&gt; It decides what to deliver. A channel groups one or more event types, and each event type is added as a channel member. In this example the channel has one member, &lt;code&gt;Customer_Created__e&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event relay.&lt;/strong&gt; It connects one channel to one named credential, and it holds the recovery setting described in Recovery after an outage. A new relay starts out &lt;a href="https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_eventrelayconfig.htm" rel="noopener noreferrer"&gt;stopped&lt;/a&gt;. After it's created, you can change only its state and recovery setting. A wrong channel or credential means deleting the relay and creating it again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you create the relay, Salesforce registers a &lt;a href="https://help.salesforce.com/s/articleView?id=platform.ev_relay_create_ui.htm&amp;amp;language=en_US&amp;amp;type=5" rel="noopener noreferrer"&gt;partner event source&lt;/a&gt; in your AWS account, in a pending state. You associate that source with an event bus in AWS, then start the relay in Salesforce. Events start flowing at that point. That bus is a partner event bus, and it receives only the events from this relay. One relay maps to one channel, one partner event source, and one partner bus.&lt;/p&gt;

&lt;p&gt;To configure each piece, follow Salesforce's guide &lt;a href="https://help.salesforce.com/s/articleView?id=platform.ev_relay_events_section.htm&amp;amp;language=en_US&amp;amp;type=5" rel="noopener noreferrer"&gt;Relay Events from Salesforce to Amazon EventBridge&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What arrives in AWS
&lt;/h3&gt;

&lt;p&gt;This is a relayed event as EventBridge delivers it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6f33d717-8e35-4488-5690-89e681b5737c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail-type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Customer_Created__e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws.partner/salesforce.com/00D.../0YL..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"account"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123456789012"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-13T19:23:23Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"region"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"CreatedById"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"005..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"CreatedDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-13T19:23:22.686Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Account_Id__c"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0015g00000AbCdEAAV"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Name__c"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Corp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Industry__c"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Manufacturing"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"schemaId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mVBhpA7tU9MCtBEbqjycBQ"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"118dab75-7b45-498e-8478-52d0324c1060"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;source&lt;/code&gt; contains the Salesforce org ID and channel ID.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;detail-type&lt;/code&gt; is the Salesforce event name, which is what the rule matches on.&lt;/li&gt;
&lt;li&gt;The fields are under &lt;code&gt;detail.payload&lt;/code&gt;, with their Salesforce API names.&lt;/li&gt;
&lt;li&gt;The top-level &lt;code&gt;id&lt;/code&gt; comes from EventBridge. &lt;code&gt;detail.id&lt;/code&gt; comes from Salesforce.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  EventBridge rule
&lt;/h3&gt;

&lt;p&gt;The rule matches &lt;code&gt;detail-type&lt;/code&gt; = &lt;code&gt;Customer_Created__e&lt;/code&gt; and sends those events to the SQS queue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Amazon SQS queue and DLQ
&lt;/h3&gt;

&lt;p&gt;The rule sends events to an SQS queue instead of invoking the Lambda directly, with a DLQ for messages that can't be processed. The queue absorbs bursts, lets you cap Lambda concurrency so DynamoDB isn't overloaded, and retries failed messages. Messages that keep failing stay in the DLQ, so you can redrive them after a fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lambda and DynamoDB
&lt;/h3&gt;

&lt;p&gt;The Lambda reads the customer details from the event and writes them to DynamoDB. That is where the AWS applications read the data from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Recovery after an outage
&lt;/h3&gt;

&lt;p&gt;If the relay hits an error it can't resume from, a recovery setting decides what happens (&lt;a href="https://help.salesforce.com/s/articleView?id=platform.ev_relay_error_recovery.htm&amp;amp;language=en_US&amp;amp;type=5" rel="noopener noreferrer"&gt;Error Recovery&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;LATEST&lt;/code&gt; (the default) skips events published during the outage.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EARLIEST&lt;/code&gt; re-sends everything still retained on the Salesforce event bus, up to 72 hours.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For business data, &lt;code&gt;EARLIEST&lt;/code&gt; is usually the right choice, because a missed event means a customer the AWS applications don't know about. The cost is duplicates. Between &lt;code&gt;EARLIEST&lt;/code&gt; recovery, EventBridge retries, and SQS retries, the same event can be delivered more than once, so the application must be designed to process duplicate messages. In this example, the Lambda writes with a conditional put keyed on the Account ID and treats an existing record as success, so duplicates never reach the DLQ.&lt;/p&gt;

&lt;h3&gt;
  
  
  Small payloads
&lt;/h3&gt;

&lt;p&gt;Salesforce &lt;a href="https://help.salesforce.com/s/articleView?id=platform.ev_relay_feature_support.htm&amp;amp;language=en&amp;amp;type=5" rel="noopener noreferrer"&gt;documents a 256 KB limit&lt;/a&gt; for relayed events. Larger events aren't delivered to EventBridge. Hence, the event should carry only the record ID and the fields the AWS applications need.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitoring the relay
&lt;/h3&gt;

&lt;p&gt;If the relay goes into an error state, delivery stops until Salesforce recovers. It retries periodically and resumes from where it left off, but it doesn't notify anyone (&lt;a href="https://help.salesforce.com/s/articleView?id=platform.ev_relay_status.htm&amp;amp;language=en_US&amp;amp;type=5" rel="noopener noreferrer"&gt;Relay status&lt;/a&gt;). Monitor the relay by querying &lt;code&gt;EventRelayFeedback&lt;/code&gt;, a Salesforce object that holds the relay's status and error details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost
&lt;/h2&gt;

&lt;p&gt;On the Salesforce side, &lt;a href="https://developer.salesforce.com/blogs/2022/10/salesforce-event-relay-is-generally-available" rel="noopener noreferrer"&gt;Event Relay is included at no extra cost&lt;/a&gt;. Relayed events count toward the org's daily event delivery allocation. By default, that's &lt;a href="https://developer.salesforce.com/docs/platform/platform-events/guide/platform-events-limits-entitlement.html" rel="noopener noreferrer"&gt;50,000 events a day for Unlimited and Performance editions and 25,000 for Enterprise Edition&lt;/a&gt;. If you need more, Salesforce sells a &lt;a href="https://developer.salesforce.com/docs/platform/platform-events/guide/platform-events-limits-entitlement.html" rel="noopener noreferrer"&gt;platform events add-on&lt;/a&gt; that raises it by 100,000 events a day.&lt;/p&gt;

&lt;p&gt;On the AWS side, events arriving on the partner event bus are billed as &lt;a href="https://aws.amazon.com/eventbridge/pricing/" rel="noopener noreferrer"&gt;partner events&lt;/a&gt;, at $1.00 per million events. Each 64 KB chunk of an event counts as one event.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Salesforce edition.&lt;/strong&gt; Event Relay is &lt;a href="https://developer.salesforce.com/docs/platform/event-relay/guide/intro.html" rel="noopener noreferrer"&gt;available in Enterprise, Unlimited, and Developer editions&lt;/a&gt;. It isn't available in Government Cloud. A free Developer Edition org is enough to build and test this example.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Event types.&lt;/strong&gt; Event Relay works with &lt;a href="https://help.salesforce.com/s/articleView?id=platform.ev_relay_feature_support.htm&amp;amp;language=en&amp;amp;type=5" rel="noopener noreferrer"&gt;custom high-volume platform events&lt;/a&gt;, which is the default for any new custom platform event such as &lt;code&gt;Customer_Created__e&lt;/code&gt;. It also works with Change Data Capture and Real-Time Event Monitoring events.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AWS region.&lt;/strong&gt; Event Relay can &lt;a href="https://help.salesforce.com/s/articleView?id=platform.ev_relay_considerations.htm&amp;amp;language=en_US&amp;amp;type=5" rel="noopener noreferrer"&gt;deliver only to default AWS regions&lt;/a&gt;. &lt;a href="https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-regions.html" rel="noopener noreferrer"&gt;Default regions&lt;/a&gt;, such as &lt;code&gt;us-east-1&lt;/code&gt;, are enabled in every AWS account. &lt;a href="https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-regions.html" rel="noopener noreferrer"&gt;Opt-in regions&lt;/a&gt;, such as Europe (Milan), have to be enabled first, and Event Relay doesn't support them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Delivery allocation.&lt;/strong&gt; Salesforce limits how many events an org can deliver in 24 hours, and every event the relay sends to EventBridge &lt;a href="https://help.salesforce.com/s/articleView?id=platform.ev_relay_allocations.htm&amp;amp;language=en_US&amp;amp;type=5" rel="noopener noreferrer"&gt;counts toward that limit&lt;/a&gt;. Other integrations that subscribe to events in the same org share it, so relay only the events AWS needs.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>salesforce</category>
      <category>eventbridge</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Hands-On with AWS DevOps Agent: Changing DevOps Work Without Replacing Engineers</title>
      <dc:creator>Matheus das Mercês</dc:creator>
      <pubDate>Thu, 15 Oct 2026 09:22:27 +0000</pubDate>
      <link>https://dev.to/aws-builders/hands-on-with-aws-devops-agent-changing-devops-work-without-replacing-engineers-kfe</link>
      <guid>https://dev.to/aws-builders/hands-on-with-aws-devops-agent-changing-devops-work-without-replacing-engineers-kfe</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;When an AI agent can investigate incidents, read operational context, query systems, and suggest what might be wrong, it is natural to ask whether this kind of capability will eventually replace part of the work DevOps engineers do today.&lt;/p&gt;

&lt;p&gt;In this article, I will share how we experimented with AWS DevOps Agent, why we wanted to bring the investigation flow closer to engineers through Slack. The goal is not to argue that AI agents should replace DevOps engineers, but to show how they can change the type of work engineers spend time on.&lt;/p&gt;

&lt;p&gt;The most important part is: what the engineer asks, what context the agent needs, which skills make the investigation useful, and where human judgment should remain in control.&lt;/p&gt;

&lt;h2&gt;
  
  
  The turning point
&lt;/h2&gt;

&lt;p&gt;Generative AI focuses on creating new content based on an input. The output can be text, images, audio, video, software code, or any other artifact generated from a prompt. That is already useful in many engineering workflows, especially when the task is about explaining, summarizing, drafting, or creating something from existing context.&lt;/p&gt;

&lt;p&gt;Agentic AI has a different goal. Instead of only generating content, an agent is expected to work toward an outcome. In the DevOps context, this means the agent needs to understand what is happening, use tools, inspect data, reason over the environment, and come back with findings that help the engineer decide what to do next.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgp7fj9utox7yu4xzvc8v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgp7fj9utox7yu4xzvc8v.png" alt="Agentic AI" width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This difference becomes important when the problem is not “write me a summary,” but something closer to “investigate why this service is failing.” In that case, the useful output is not only a nice explanation. The useful output is a hypothesis supported by evidence from logs, metrics, topology, recent changes, known incidents, and operational knowledge.&lt;/p&gt;

&lt;h2&gt;
  
  
  The DevOps problem we wanted to solve
&lt;/h2&gt;

&lt;p&gt;At the &lt;strong&gt;&lt;em&gt;AI Center of Excellence&lt;/em&gt;&lt;/strong&gt; team at PostNL, we build and operate AI capabilities used by multiple teams and applications. These services do not live in a single cloud environment. A single user-facing capability can depend not only on AWS services, but also services in other cloud providers, services, external APIs, networking, authentication, model providers, and internal platform components.&lt;/p&gt;

&lt;p&gt;The complexity is not necessarily the number of services involved. &lt;strong&gt;The real complexity appears when something goes wrong, and the engineer needs to understand where to look first&lt;/strong&gt;. During an incident, this can easily become a manual process of opening dashboards, checking the AWS Console, searching logs, inspecting metrics, comparing timestamps, and trying to form a hypothesis from many disconnected signals.&lt;/p&gt;

&lt;p&gt;This is especially painful during on-call situations. When an engineer is woken up at night, the first minutes are often spent collecting evidence instead of making decisions. The engineer still needs to own the decision, but there is a lot of repetitive investigation work before reaching the point where human judgment is most valuable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we chose AWS DevOps Agent
&lt;/h2&gt;

&lt;p&gt;When you look at this kind of problem, it is tempting to say that you should simply build an agent. However, building an agent is not only about sending a prompt to a model. You need to define, among other things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What the agent should achieve;&lt;/li&gt;
&lt;li&gt;Which systems it can access;&lt;/li&gt;
&lt;li&gt;Which tools it can use; &lt;/li&gt;
&lt;li&gt;What information it should remember;&lt;/li&gt;
&lt;li&gt;How much autonomy it should have;&lt;/li&gt;
&lt;li&gt;Which actions require approval;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In our case, we wanted the benefit of an operational investigation agent, but we did not want to become the team responsible for maintaining the entire investigation engine ourselves. We needed something that could understand resources, dependencies, operational patterns, and topology, while still allowing us to shape the workflow around our own environment.&lt;/p&gt;

&lt;p&gt;This is where AWS DevOps Agent became interesting. Instead of building the investigation engine from scratch, we could use the managed capability as the core and focus on the thin layer around it. That layer was still important, but it was much closer to our actual problem: making the investigation accessible from where engineers already work and giving the agent the right operational context.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we built around it
&lt;/h2&gt;

&lt;p&gt;One of the decisions we made early was that we did not want another dashboard. During incidents, engineers are already communicating in Slack, so asking them to open another tool just to start the investigation did not feel like the right experience.&lt;/p&gt;

&lt;p&gt;The idea was to create a Slack bot that could act as the entry point for investigations. An engineer could ask something like &lt;code&gt;/watchdog investigate the 429s we are seeing&lt;/code&gt;, and the bot would trigger the investigation flow with AWS DevOps Agent. The result was not meant to remove the engineer from the process, but to reduce the amount of manual evidence gathering before the engineer could make a decision.&lt;/p&gt;

&lt;p&gt;The architecture was intentionally thin around the managed capability:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Slack&lt;/strong&gt; was the interface where engineers already worked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;/watchdog&lt;/code&gt; command&lt;/strong&gt; was the trigger for the investigation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A small Lambda function behind an API Gateway&lt;/strong&gt; translated the Slack request into an investigation request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS DevOps Agent&lt;/strong&gt; performed the investigation using the operational context available to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Slack thread&lt;/strong&gt; became the place where findings and updates were shared back with the engineer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffs6ul8od5zs0w7qax4ge.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffs6ul8od5zs0w7qax4ge.png" alt="slack architecture" width="583" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The agent itself was also defined as infrastructure. The important part was to define the AWS DevOps Agent space and the role it could assume to investigate the AWS account. The cool thing is that you can control exactly what resources the agent is allowed to investigate.&amp;nbsp;&lt;/p&gt;

&lt;p&gt;One limitation we had to account for is that not everything in AWS DevOps Agent was available through CDK or CloudFormation. The Agent Space and AWS account association could be defined as infrastructure, but skills still had to be configured outside CDK at that point. That means the infrastructure could create the managed agent foundation, while the operational knowledge attached to the agent still required a separate setup step in the console.&lt;/p&gt;

&lt;p&gt;In a simplified version of the CDK code, the core definition of the AWS DevOps Agent space looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-cdk-lib&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-cdk-lib/aws-iam&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Construct&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;constructs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DevOpsAgentStack&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Stack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Construct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StackProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agentSpaceArnPattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`arn:aws:aidevops:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:agentspace/*`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;monitorRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Role&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MonitorRole&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;assumedBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ServicePrincipal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aidevops.amazonaws.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;conditions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;StringEquals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws:SourceAccount&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;ArnLike&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws:SourceArn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;agentSpaceArnPattern&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="na"&gt;managedPolicies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ManagedPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromAwsManagedPolicyName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AIDevOpsAgentAccessPolicy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;monitorRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PolicyStatement&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;iam:CreateServiceLinkedRole&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="s2"&gt;`arn:aws:iam::&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:role/aws-service-role/resource-explorer-2.amazonaws.com/AWSServiceRoleForResourceExplorer`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agentSpace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AgentSpace&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AWS::DevOpsAgent::AgentSpace&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;example-devops-agent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DevOps investigation agent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AwsMonitorAssociation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AWS::DevOpsAgent::Association&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;AgentSpaceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;agentSpace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;ServiceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;Aws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;AccountId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;AccountType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;monitor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;AssumableRoleArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;monitorRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roleArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gave us a workflow where the agent could do the first investigation pass, but the engineer still owned the interpretation and the next step. That distinction was important because the goal was not to create a fully autonomous incident response system, but to reduce the repetitive work required to understand what was happening.&lt;/p&gt;

&lt;h2&gt;
  
  
  The skills we defined for the agent
&lt;/h2&gt;

&lt;p&gt;The Slack bot request handler was only one part of the solution. The more important part was the context we provided to the AWS DevOps Agent space. A generic investigation agent can be useful, but real environments have details that are specific to the organization, the platform, the architecture, and the way teams operate.&lt;/p&gt;

&lt;p&gt;For that reason, we defined custom skills and knowledge sources to help the agent understand our environment better. This can be easily created in the AWS DevOps agent space web app. Some examples were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sample-skip-scheduled-maintenance&lt;/code&gt;&lt;/strong&gt;: used to skip low-priority incidents during a scheduled maintenance window and avoid unnecessary investigations for expected disruptions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;azure-troubleshooting-guide&lt;/code&gt;&lt;/strong&gt;: used to give the agent troubleshooting context for Azure-related incidents, performance issues, and dependencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;understanding-ai-toolkit-architecture&lt;/code&gt;&lt;/strong&gt;: used to document the architecture on Azure, workloads, resource groups, Function Apps, Cognitive Services, networking, and related components.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;understanding-lpe-architecture&lt;/code&gt;&lt;/strong&gt;: used to document the LandingZone PostNL Engineering infrastructure, including networking, IAM, monitoring, and alerting context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the kinds of details that an engineer usually learns over time, but that an agent also needs if we expect it to produce useful findings.&lt;/p&gt;

&lt;p&gt;This is where DevOps engineers still play a critical role. The value is not only in connecting an agent to tools, but in deciding which context the agent needs, which operational assumptions are safe, which sources of information are reliable, and which parts of the investigation should remain read-only or require approval.&lt;/p&gt;

&lt;h2&gt;
  
  
  AWS released native Slack support
&lt;/h2&gt;

&lt;p&gt;There is an interesting detail in this story. We built the Slack bot because we wanted to bring the investigation flow to where engineers already work, and at that moment this meant creating our own thin integration between Slack and AWS DevOps Agent.&lt;/p&gt;

&lt;p&gt;Later, AWS released native bidirectional Slack communication for AWS DevOps Agent. According to the &lt;a href="https://docs.aws.amazon.com/devopsagent/latest/userguide/whats-new.html" rel="noopener noreferrer"&gt;AWS DevOps Agent What's New page&lt;/a&gt;, on September 1, 2026, AWS announced that you can start and continue conversations with AWS DevOps Agent directly from a private Slack channel by enabling bidirectional communication when associating a channel.&lt;/p&gt;

&lt;p&gt;At first glance, it might look like this makes the custom Slack bot unnecessary. In practice, it makes the communication layer simpler, which is a good thing. The bot solved the interface gap we had at the time, but the reusable part of the work was not only the bot implementation. It was the understanding of how engineers wanted to interact with an investigation agent during an incident.&lt;/p&gt;

&lt;p&gt;We still needed to define what engineers ask during incidents, what the agent should know, which skills improve investigations, and where the boundary between recommendation and action should be. If the native Slack integration covers the communication layer, the surrounding concepts can still help with additional context, specialized knowledge, Bedrock-based extensions, knowledge bases, or other systems that provide the agent with better input for the investigation, for example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft7o5qx8cwunrohlq4v1e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft7o5qx8cwunrohlq4v1e.png" alt=" " width="799" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Before and after
&lt;/h2&gt;

&lt;p&gt;The most important change is not that the engineer disappears from the workflow. The change is that the engineer starts from a better point in the investigation.&lt;/p&gt;

&lt;p&gt;Before, the engineer had to manually collect evidence across multiple systems. That meant opening dashboards, checking cloud consoles, searching logs, comparing timestamps, and trying to understand whether the issue was related to AWS, Azure, an external dependency, networking, authentication, or something else. This work is important, but it is also repetitive and time-sensitive.&lt;/p&gt;

&lt;p&gt;The old flow looked closer to this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Wake up because something might be wrong or the engineer got a phone call.&lt;/li&gt;
&lt;li&gt;Open dashboards and cloud consoles.&lt;/li&gt;
&lt;li&gt;Check logs, metrics, alarms, and recent signals.&lt;/li&gt;
&lt;li&gt;Compare timestamps across systems.&lt;/li&gt;
&lt;li&gt;Build a first hypothesis manually.&lt;/li&gt;
&lt;li&gt;Decide what to investigate next.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After introducing the agentic investigation flow, the engineer can ask the agent to investigate and then review the findings. The agent can search logs, query resources, correlate signals, and build an initial hypothesis. The engineer still evaluates the result, considers the business impact, and decides what should happen next.&lt;/p&gt;

&lt;p&gt;The new flow became closer to this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ask the agent to investigate in Slack.&lt;/li&gt;
&lt;li&gt;Let the agent collect operational signals and context.&lt;/li&gt;
&lt;li&gt;Review the findings and hypothesis.&lt;/li&gt;
&lt;li&gt;Decide what action, if any, should be taken.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the part of DevOps work that I think is worth changing. We should not try to automate judgment without understanding the risk, but we should reduce the amount of manual work required before judgment can happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should stay human
&lt;/h2&gt;

&lt;p&gt;Even if an agent can investigate an issue, not every action should be delegated without approval. There is a big difference between collecting evidence and changing production systems.&lt;/p&gt;

&lt;p&gt;In a DevOps workflow, humans should remain responsible for decisions where context, risk, and business impact matter. For example, I would not put these actions in the same category as querying logs or summarizing metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;restarting critical services&lt;/li&gt;
&lt;li&gt;changing infrastructure&lt;/li&gt;
&lt;li&gt;disabling alarms&lt;/li&gt;
&lt;li&gt;modifying permissions&lt;/li&gt;
&lt;li&gt;applying mitigations that affect customers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These actions require a different level of control.&lt;/p&gt;

&lt;p&gt;This is why I believe the most useful agentic workflows are not necessarily the ones with full autonomy. A workflow where the agent investigates, correlates, and proposes a hypothesis can already remove a lot of toil. The engineer can stay responsible for the decision while spending less time collecting the evidence manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;AWS DevOps Agent is interesting to me because it can change the kind of DevOps work engineers spend time on. &lt;strong&gt;The goal is not to remove engineers from the process, but to reduce the repetitive investigation work that happens before engineers can make good decisions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In our experiment, the Slack bot was a way to bring the investigation flow &lt;strong&gt;closer to where engineers already work&lt;/strong&gt;. With the native bidirectional Slack support now available in AWS DevOps Agent, that communication layer can become simpler, but the core lessons remain useful. The operational context, the skills, the knowledge sources, and the approval boundaries are still the parts that make the workflow valuable.&lt;/p&gt;

&lt;p&gt;The opportunity is to let the agent help with logs, signals, topology, and hypotheses while &lt;strong&gt;keeping engineers responsible for context, risk, and business impact&lt;/strong&gt;. In other words, use AWS DevOps Agent to change DevOps work, not to replace DevOps engineers.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>aws</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Using Firehose as a Lambda Log Destination</title>
      <dc:creator>Sunny Bhambhani</dc:creator>
      <pubDate>Thu, 15 Oct 2026 07:43:05 +0000</pubDate>
      <link>https://dev.to/aws-builders/using-firehose-as-a-lambda-log-destination-22po</link>
      <guid>https://dev.to/aws-builders/using-firehose-as-a-lambda-log-destination-22po</guid>
      <description>&lt;h1&gt;
  
  
  Using Firehose as a Lambda Log Destination
&lt;/h1&gt;

&lt;p&gt;The last logging option available in the Lambda configuration is Amazon Data Firehose.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbjdex5fco9stzqphe7vp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbjdex5fco9stzqphe7vp.png" alt=" " width="800" height="143"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This option becomes interesting when CloudWatch Logs is not the final destination and you want Lambda logs to keep moving through a delivery pipeline.&lt;/p&gt;

&lt;p&gt;Instead of just storing logs in one place, Firehose is built to receive data, optionally transform it, and then deliver it to another system.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Firehose works at a high level
&lt;/h2&gt;

&lt;p&gt;The flow is fairly straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ingest: choose where the incoming data comes from, such as Amazon Kinesis Data Streams, Amazon MSK, or Direct PUT&lt;/li&gt;
&lt;li&gt;Transform: optionally process or enrich the data before delivery&lt;/li&gt;
&lt;li&gt;Deliver: send the data to a destination such as Amazon S3, Splunk, Dynatrace, and others&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That flexibility is the main reason Firehose is useful. It can sit between the source and the final storage or observability platform without needing you to build the delivery logic yourself.&lt;/p&gt;

&lt;p&gt;For this example, I kept things simple and used Amazon S3 as the destination.&lt;/p&gt;

&lt;p&gt;If you want to see the full list of supported destinations, AWS documents them here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/firehose/latest/dev/create-destination.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/firehose/latest/dev/create-destination.html&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Firehose stream
&lt;/h2&gt;

&lt;p&gt;Before sending Lambda logs to Firehose, I created a delivery stream with the following setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Source: &lt;code&gt;Direct PUT&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Destination: &lt;code&gt;Amazon S3&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;ARN: &lt;code&gt;arn:aws:firehose:eu-central-1:ACCOUNT_ID:deliverystream/PUT-S3-QdzyI&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Make sure the status is &lt;code&gt;Active&lt;/code&gt; after it is created.&lt;/p&gt;

&lt;h2&gt;
  
  
  Destination settings
&lt;/h2&gt;

&lt;p&gt;The S3 delivery stream was configured like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;S3 bucket: &lt;code&gt;s3-lambda-logs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;S3 bucket error output prefix: &lt;code&gt;fh_err_&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;S3 bucket prefix: &lt;code&gt;fh_&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;S3 bucket and error output prefix time zone: &lt;code&gt;UTC&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;New line delimiter: &lt;code&gt;Enabled&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This setup is useful because it separates normal output from delivery errors and keeps the objects written to S3 organized under a predictable prefix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the logs land in S3
&lt;/h2&gt;

&lt;p&gt;After the stream is configured and Lambda starts sending logs through Firehose, the delivered files appear in S3 under a path like this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Amazon S3 -&amp;gt; Buckets -&amp;gt; s3-lambda-logs -&amp;gt; fh_2026/ -&amp;gt; 10/ -&amp;gt; 11/ -&amp;gt; 12/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That structure makes it easier to find logs by date and process them later with downstream tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick note about delivery log classes
&lt;/h2&gt;

&lt;p&gt;If you send logs to Amazon S3 or Amazon Data Firehose, Lambda requires a log group with the delivery log class.&lt;/p&gt;

&lt;p&gt;That is an important detail because this log class is designed for delivery, not for the full CloudWatch Logs experience. In other words, you trade some CloudWatch features for simpler forwarding.&lt;/p&gt;

&lt;p&gt;For example, delivery log class does not support standard CloudWatch Logs features such as Logs Insights and Live Tail.&lt;/p&gt;

&lt;p&gt;It is also worth remembering that log events are only kept in CloudWatch Logs for two days when this class is used. That retention period is fixed and cannot be changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Function
&lt;/h2&gt;

&lt;p&gt;The function remains the same which we discussed in the first article.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dummy Lambda output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dummy Lambda output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;({},&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  In this example:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A new log group is created named &lt;code&gt;dummy_lambda_logs_to_firehose&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;I also created an IAM role for the Lambda function. The trust relationship and permissions are defined below:

&lt;ul&gt;
&lt;li&gt;03_iam_role_trust_relationships.json
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"logs.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"StringLike"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"aws:SourceArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:logs:eu-central-1:ACCOUNT_ID:*"&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;03_iam_role_permissiones.json
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"firehose:PutRecord"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"firehose:PutRecordBatch"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:firehose:eu-central-1:ACCOUNT_ID:deliverystream/*"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes:&lt;/p&gt;

&lt;p&gt;Log delivery to Amazon Data Firehose requires using a log group with the delivery log class. This enables efficient routing, but it does not support standard CloudWatch Logs features such as Logs Insights and Live Tail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;If CloudWatch Logs is where you troubleshoot, S3 is where you archive, then Firehose is the option you use when logs need to travel.&lt;/p&gt;

&lt;p&gt;For a simple demo, using Firehose with S3 keeps the architecture easy to understand. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In a larger environment, the same pattern can be extended to feed analytics, security, or observability platforms without changing the Lambda function itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feel free to add your thoughts, happy learning!&lt;/p&gt;

</description>
      <category>lambda</category>
      <category>logging</category>
      <category>operations</category>
      <category>aws</category>
    </item>
  </channel>
</rss>
